From 99fac109df2d701051b5de474c66a0f992134183 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 7 Oct 2025 16:00:21 +0200 Subject: [PATCH 01/22] Validate certificate identity from cross cluster creds --- ...erSecurityCrossClusterApiKeySigningIT.java | 145 +++++++++++++++--- .../xpack/security/authc/ApiKeyService.java | 61 ++++++-- ...ossClusterAccessAuthenticationService.java | 29 ++-- .../authc/CrossClusterAccessHeaders.java | 23 ++- .../CrossClusterApiKeySignatureManager.java | 12 +- ...ossClusterApiKeySigningConfigReloader.java | 1 - .../security/authc/ApiKeyServiceTests.java | 127 +++++++++------ ...AccessAuthenticationServiceIntegTests.java | 125 +++++++++++++-- 8 files changed, 412 insertions(+), 111 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java index 446a6c546b509..e2ad0f67a3bbf 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.remotecluster; import io.netty.handler.codec.http.HttpMethod; - import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; @@ -25,6 +24,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 +38,7 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase { - private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); + private static final AtomicReference> MY_REMOTE_API_KEY_MAP_REF = new AtomicReference<>(); static { fulfillingCluster = ElasticsearchCluster.local() @@ -49,8 +49,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 +64,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 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 +93,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(); - 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 [("); + + // 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(); - // TODO add test for certificate identity not configured for API key but signature provided (should 200) + // Reset skip_unavailable + updateClusterSettings( + Settings.builder().put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(false)).build() + ); - // TODO add test for certificate identity not configured for API key but wrong signature provided (should 401) + // 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 regex matching (should 200) + // 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()); + 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 +303,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 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); + } + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index c0d00c3838597..234ab14470ff2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -141,6 +141,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.elasticsearch.common.SecureRandomUtils.getBase64SecureRandomString; @@ -1396,7 +1397,7 @@ void validateApiKeyCredentials( if (result.success) { if (result.verify(credentials.getKey())) { // move on - validateApiKeyTypeAndExpiration(apiKeyDoc, credentials, clock, listener); + completeApiKeyAuthentication(apiKeyDoc, credentials, clock, listener); } else { listener.onResponse( AuthenticationResult.unsuccessful("invalid credentials for API key [" + credentials.getId() + "]", null) @@ -1416,7 +1417,7 @@ void validateApiKeyCredentials( listenableCacheEntry.onResponse(new CachedApiKeyHashResult(verified, credentials.getKey())); if (verified) { // move on - validateApiKeyTypeAndExpiration(apiKeyDoc, credentials, clock, listener); + completeApiKeyAuthentication(apiKeyDoc, credentials, clock, listener); } else { listener.onResponse( AuthenticationResult.unsuccessful("invalid credentials for API key [" + credentials.getId() + "]", null) @@ -1439,7 +1440,7 @@ void validateApiKeyCredentials( verifyKeyAgainstHash(apiKeyDoc.hash, credentials, ActionListener.wrap(verified -> { if (verified) { // move on - validateApiKeyTypeAndExpiration(apiKeyDoc, credentials, clock, listener); + completeApiKeyAuthentication(apiKeyDoc, credentials, clock, listener); } else { listener.onResponse( AuthenticationResult.unsuccessful("invalid credentials for API key [" + credentials.getId() + "]", null) @@ -1471,7 +1472,7 @@ Cache getRoleDescriptorsBytesCache() { } // package-private for testing - static void validateApiKeyTypeAndExpiration( + static void completeApiKeyAuthentication( ApiKeyDoc apiKeyDoc, ApiKeyCredentials credentials, Clock clock, @@ -1491,6 +1492,27 @@ static void validateApiKeyTypeAndExpiration( return; } + if (apiKeyDoc.certificateIdentity != null) { + if (credentials.getCertificateIdentity() == null) { + listener.onResponse( + AuthenticationResult.terminate("Expected signature for cross cluster API key, but no signature was provided") + ); + return; + } + if (validateCertificateIdentity(credentials.getCertificateIdentity(), apiKeyDoc.certificateIdentity) == false) { + listener.onResponse( + AuthenticationResult.terminate( + Strings.format( + "DN from provided certificate [%s] does not match API Key certificate identity pattern [%s]", + credentials.getCertificateIdentity(), + apiKeyDoc.certificateIdentity + ) + ) + ); + return; + } + } + if (apiKeyDoc.expirationTime == -1 || Instant.ofEpochMilli(apiKeyDoc.expirationTime).isAfter(clock.instant())) { final String principal = Objects.requireNonNull((String) apiKeyDoc.creator.get("principal")); final String fullName = (String) apiKeyDoc.creator.get("full_name"); @@ -1515,22 +1537,32 @@ static void validateApiKeyTypeAndExpiration( } } + private static boolean validateCertificateIdentity(String certificateIdentity, String certificateIdentityPattern) { + logger.trace("Validating certificate identity [{}] against [{}]", certificateIdentity, certificateIdentityPattern); + // Consider adding a cache if this causes performance problems + return Pattern.compile(certificateIdentityPattern).matcher(certificateIdentity).matches(); + } + ApiKeyCredentials parseCredentialsFromApiKeyString(SecureString apiKeyString) { if (false == isEnabled()) { return null; } - return parseApiKey(apiKeyString, ApiKey.Type.REST); + return parseApiKey(apiKeyString, null, ApiKey.Type.REST); } - static ApiKeyCredentials getCredentialsFromHeader(final String header, ApiKey.Type expectedType) { - return parseApiKey(Authenticator.extractCredentialFromHeaderValue(header, "ApiKey"), expectedType); + static ApiKeyCredentials getCredentialsFromHeader(final String header, @Nullable String certificateIdentity, ApiKey.Type expectedType) { + return parseApiKey(Authenticator.extractCredentialFromHeaderValue(header, "ApiKey"), certificateIdentity, expectedType); } public static String withApiKeyPrefix(final String encodedApiKey) { return "ApiKey " + encodedApiKey; } - private static ApiKeyCredentials parseApiKey(SecureString apiKeyString, ApiKey.Type expectedType) { + private static ApiKeyCredentials parseApiKey( + SecureString apiKeyString, + @Nullable String certificateIdentity, + ApiKey.Type expectedType + ) { if (apiKeyString != null) { final byte[] decodedApiKeyCredBytes = Base64.getDecoder().decode(CharArrays.toUtf8Bytes(apiKeyString.getChars())); char[] apiKeyCredChars = null; @@ -1554,7 +1586,8 @@ private static ApiKeyCredentials parseApiKey(SecureString apiKeyString, ApiKey.T return new ApiKeyCredentials( new String(Arrays.copyOfRange(apiKeyCredChars, 0, colonIndex)), new SecureString(Arrays.copyOfRange(apiKeyCredChars, secretStartPos, apiKeyCredChars.length)), - expectedType + expectedType, + certificateIdentity ); } finally { if (apiKeyCredChars != null) { @@ -1671,11 +1704,17 @@ public static final class ApiKeyCredentials implements AuthenticationToken, Clos private final String id; private final SecureString key; private final ApiKey.Type expectedType; + private final String certificateIdentity; public ApiKeyCredentials(String id, SecureString key, ApiKey.Type expectedType) { + this(id, key, expectedType, null); + } + + public ApiKeyCredentials(String id, SecureString key, ApiKey.Type expectedType, @Nullable String certificateIdentity) { this.id = id; this.key = key; this.expectedType = expectedType; + this.certificateIdentity = certificateIdentity; } String getId() { @@ -1709,6 +1748,10 @@ public void clearCredentials() { public ApiKey.Type getExpectedType() { return expectedType; } + + public String getCertificateIdentity() { + return certificateIdentity; + } } private static class ApiKeyLoggingDeprecationHandler implements DeprecationHandler { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java index e5a2b7a53a817..63075f1337dba 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java @@ -37,6 +37,8 @@ import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY; import static org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY; import static org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders.CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY; +import static org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders.getCertificateIdentity; +import static org.elasticsearch.xpack.security.transport.X509CertificateSignature.CROSS_CLUSTER_ACCESS_SIGNATURE_HEADER_KEY; public class CrossClusterAccessAuthenticationService implements RemoteClusterAuthenticationService { @@ -71,18 +73,17 @@ public void authenticate(final String action, final TransportRequest request, fi assert ApiKey.Type.CROSS_CLUSTER == apiKeyCredentials.getExpectedType(); // authn must verify only the provided api key and not try to extract any other credential from the thread context authcContext = authenticationService.newContext(action, request, apiKeyCredentials); + var signature = crossClusterAccessHeaders.signature(); + + // Always validate a signature if provided + if (signature != null && verifySignature(authcContext, signature, crossClusterAccessHeaders, listener) == false) { + return; + } } catch (Exception ex) { withRequestProcessingFailure(authenticationService.newContext(action, request, null), ex, listener); return; } - // TODO ALWAYS check if used api key has a certificate identity and do this verification conditionally based on that - var signature = crossClusterAccessHeaders.signature(); - // Always validate a signature if provided - if (signature != null && verifySignature(authcContext, signature, crossClusterAccessHeaders, listener) == false) { - return; - } - try { apiKeyService.ensureEnabled(); } catch (Exception ex) { @@ -120,7 +121,6 @@ public void authenticate(final String action, final TransportRequest request, fi new ContextPreservingActionListener<>(storedContextSupplier, ActionListener.wrap(authentication -> { assert authentication.isApiKey() : "initial authentication for cross cluster access must be by API key"; assert false == authentication.isRunAs() : "initial authentication for cross cluster access cannot be run-as"; - // try-catch so any failure here is wrapped by `withRequestProcessingFailure`, whereas `authenticate` failures are not // we should _not_ wrap `authenticate` failures since this produces duplicate audit events try { @@ -158,7 +158,7 @@ private boolean verifySignature( ); } if (authException != null) { - // TODO Verify this covers all audit logging scenarios + // TODO handle audit logging listener.onFailure(context.getRequest().exceptionProcessingRequest(authException, context.getMostRecentAuthenticationToken())); return false; } @@ -186,7 +186,7 @@ void tryAuthenticate(ApiKeyService.ApiKeyCredentials credentials, ActionListener listener.onResponse(null); return; } - + // TODO handle audit logging if (authResult.getStatus() == AuthenticationResult.Status.TERMINATE) { Exception e = (authResult.getException() != null) ? authResult.getException() @@ -216,7 +216,14 @@ public ApiKeyService.ApiKeyCredentials extractApiKeyCredentialsFromHeaders(Map ApiKeyService.getCredentialsFromHeader( "ApiKey " + Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8)), + null, ApiKey.Type.CROSS_CLUSTER ) ); @@ -3079,7 +3080,7 @@ public void testAuthenticationFailureWithApiKeyTypeMismatch() throws Exception { assertThat(service.getApiKeyAuthCache().keys(), contains(id)); } - public void testValidateApiKeyTypeAndExpiration() throws IOException { + public void testCompleteApiKeyAuthentication() throws IOException { final var apiKeyId = randomAlphaOfLength(12); final var apiKey = randomAlphaOfLength(16); final var hasher = getFastStoredHashAlgoForTests(); @@ -3100,7 +3101,7 @@ public void testValidateApiKeyTypeAndExpiration() throws IOException { final ApiKey.Type expectedType1 = randomValueOtherThan(apiKeyDoc1.type, () -> randomFrom(ApiKey.Type.values())); final ApiKeyCredentials apiKeyCredentials1 = getApiKeyCredentials(apiKeyId, apiKey, expectedType1); final PlainActionFuture> future1 = new PlainActionFuture<>(); - ApiKeyService.validateApiKeyTypeAndExpiration(apiKeyDoc1, apiKeyCredentials1, clock, future1); + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc1, apiKeyCredentials1, clock, future1); final AuthenticationResult auth1 = future1.actionGet(); assertThat(auth1.getStatus(), is(AuthenticationResult.Status.TERMINATE)); assertThat(auth1.getValue(), nullValue()); @@ -3121,7 +3122,7 @@ public void testValidateApiKeyTypeAndExpiration() throws IOException { final var apiKeyDoc2 = buildApiKeyDoc(hash, pastTime, false, -1, randomAlphaOfLengthBetween(3, 8), Version.CURRENT.id); final ApiKeyCredentials apiKeyCredentials2 = getApiKeyCredentials(apiKeyId, apiKey, apiKeyDoc2.type); final PlainActionFuture> future2 = new PlainActionFuture<>(); - ApiKeyService.validateApiKeyTypeAndExpiration(apiKeyDoc2, apiKeyCredentials2, clock, future2); + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc2, apiKeyCredentials2, clock, future2); final AuthenticationResult auth2 = future2.actionGet(); assertThat(auth2.getStatus(), is(AuthenticationResult.Status.CONTINUE)); assertThat(auth2.getValue(), nullValue()); @@ -3138,7 +3139,7 @@ public void testValidateApiKeyTypeAndExpiration() throws IOException { ); final ApiKeyCredentials apiKeyCredentials3 = getApiKeyCredentials(apiKeyId, apiKey, apiKeyDoc3.type); final PlainActionFuture> future3 = new PlainActionFuture<>(); - ApiKeyService.validateApiKeyTypeAndExpiration(apiKeyDoc3, apiKeyCredentials3, clock, future3); + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc3, apiKeyCredentials3, clock, future3); final AuthenticationResult auth3 = future3.actionGet(); assertThat(auth3.getStatus(), is(AuthenticationResult.Status.SUCCESS)); assertThat(auth3.getValue(), notNullValue()); @@ -3210,8 +3211,6 @@ public void testValidateOwnerUserRoleDescriptorsWithWorkflowsRestriction() { public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Exception { final String apiKeyId = randomAlphaOfLength(12); - final Clock mockClock = mock(Clock.class); - when(mockClock.instant()).thenReturn(Instant.now()); // Scenario 1: Update with a new value { @@ -3230,7 +3229,7 @@ public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Ex createTestAuthentication(), updateRequest, Set.of(), - mockClock + clock ); assertThat(builder, notNullValue()); final Map updatedDoc = extractDocumentContent(builder); @@ -3253,7 +3252,7 @@ public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Ex createTestAuthentication(), updateRequest, Set.of(), - mockClock + clock ); assertThat(builder, nullValue()); } @@ -3274,7 +3273,7 @@ public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Ex createTestAuthentication(), updateRequest, Set.of(), - mockClock + clock ); assertThat(builder, notNullValue()); final Map updatedDoc = extractDocumentContent(builder); @@ -3297,7 +3296,7 @@ public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Ex createTestAuthentication(), updateRequest, Set.of(), - mockClock + clock ); assertThat(builder, notNullValue()); final Map updatedDoc = extractDocumentContent(builder); @@ -3319,12 +3318,82 @@ public void testMaybeBuildUpdatedDocumentCertificateIdentityHandling() throws Ex createTestAuthentication(), updateRequest, Set.of(), - mockClock + clock ); assertThat(builder, nullValue()); } } + public void testCrossClusterApiKeyCertificateIdentityValidationSuccessful() throws Exception { + final String certificateIdentityPattern = "CN=(remote-cluster|test)-.*,OU=engineering,DC=example,DC=com"; + final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); + + final String matchingCertificateIdentity = "CN=remote-cluster-node1,OU=engineering,DC=example,DC=com"; + final ApiKeyCredentials credentials = new ApiKeyCredentials( + randomAlphaOfLength(12), + randomSecureStringOfLength(16), + ApiKey.Type.CROSS_CLUSTER, + matchingCertificateIdentity + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + } + + public void testCrossClusterApiKeyCertificateIdentityValidationNoMatch() throws Exception { + final String certificateIdentityPattern = "CN=(remote-cluster|test)-.*,OU=engineering,DC=example,DC=com"; + + final String nonMatchingCertificateIdentity = "CN=unknown-host,OU=other,DC=different,DC=com"; + final ApiKeyCredentials credentials = new ApiKeyCredentials( + randomAlphaOfLength(12), + randomSecureStringOfLength(16), + ApiKey.Type.CROSS_CLUSTER, + nonMatchingCertificateIdentity + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); + + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.TERMINATE)); + assertThat( + result.getMessage(), + containsString( + "DN from provided certificate [" + + nonMatchingCertificateIdentity + + "] does not match API Key certificate identity pattern [" + + certificateIdentityPattern + + "]" + ) + ); + } + + public void testCrossClusterApiKeyCertificateIdentityValidationNoCertIdentity() throws Exception { + final String certificateIdentityPattern = "CN=(remote-cluster|test)-.*,OU=engineering,DC=example,DC=com"; + final ApiKeyCredentials credentialsWithoutCertIdentity = new ApiKeyCredentials( + randomAlphaOfLength(12), + randomSecureStringOfLength(16), + ApiKey.Type.CROSS_CLUSTER + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); + + ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentialsWithoutCertIdentity, clock, future); + + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.TERMINATE)); + assertThat(result.getMessage(), equalTo("Expected signature for cross cluster API key, but no signature was provided")); + } + private static RoleDescriptor randomRoleDescriptorWithRemotePrivileges() { return new RoleDescriptor( randomAlphaOfLengthBetween(3, 90), @@ -3385,7 +3454,7 @@ public static Authentication createApiKeyAuthentication( ) ); PlainActionFuture> authenticationResultFuture = new PlainActionFuture<>(); - ApiKeyService.validateApiKeyTypeAndExpiration( + ApiKeyService.completeApiKeyAuthentication( apiKeyDoc, new ApiKeyService.ApiKeyCredentials("id", new SecureString(randomAlphaOfLength(16).toCharArray()), ApiKey.Type.REST), Clock.systemUTC(), @@ -3434,37 +3503,6 @@ public static Authentication createApiKeyAuthentication(ApiKeyService apiKeyServ } } - private ApiKeyService createApiKeyService(Settings baseSettings, FeatureService customFeatureService) { - final Settings settings = Settings.builder() - .put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true) - .put(baseSettings) - .build(); - final ClusterSettings clusterSettings = new ClusterSettings( - settings, - Sets.union( - ClusterSettings.BUILT_IN_CLUSTER_SETTINGS, - Set.of(ApiKeyService.DELETE_RETENTION_PERIOD, ApiKeyService.DELETE_INTERVAL) - ) - ); - final ApiKeyService service = new ApiKeyService( - settings, - clock, - client, - securityIndex, - ClusterServiceUtils.createClusterService(threadPool, clusterSettings), - cacheInvalidatorRegistry, - threadPool, - MeterRegistry.NOOP, - customFeatureService // Use the provided FeatureService - ); - if ("0s".equals(settings.get(ApiKeyService.CACHE_TTL_SETTING.getKey()))) { - verify(cacheInvalidatorRegistry, never()).registerCacheInvalidator(eq("api_key"), any()); - } else { - verify(cacheInvalidatorRegistry).registerCacheInvalidator(eq("api_key"), any()); - } - return service; - } - private ApiKeyService createApiKeyService() { final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); return createApiKeyService(settings); @@ -3615,7 +3653,7 @@ private ApiKey.Type parseTypeFromSourceMap(Map sourceMap) { } } - private ApiKeyDoc createCrossClusterApiKeyDocWithCertificateIdentity(String certificateIdentity) throws IOException { + private ApiKeyDoc createCrossClusterApiKeyDocWithCertificateIdentity(String certificateIdentity) { final String apiKey = randomAlphaOfLength(16); final char[] hash = getFastStoredHashAlgoForTests().hash(new SecureString(apiKey.toCharArray())); @@ -3690,4 +3728,5 @@ private static Authenticator.Context getAuthenticatorContext(ThreadContext threa private static ApiKey.Version randomApiKeyVersion() { return new ApiKey.Version(randomIntBetween(1, ApiKey.CURRENT_API_KEY_VERSION.version())); } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java index c93641fdfddba..7d7af40726a4d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java @@ -12,17 +12,21 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.ssl.PemUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; +import org.elasticsearch.xpack.core.security.action.apikey.CertificateIdentity; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; @@ -31,11 +35,16 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.user.InternalUsers; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignatureManager; +import org.elasticsearch.xpack.security.transport.X509CertificateSignature; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; @@ -45,9 +54,13 @@ import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.WAIT_UNTIL; import static org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY; import static org.elasticsearch.xpack.security.authc.CrossClusterAccessHeaders.CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY; +import static org.elasticsearch.xpack.security.transport.X509CertificateSignature.CROSS_CLUSTER_ACCESS_SIGNATURE_HEADER_KEY; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CrossClusterAccessAuthenticationServiceIntegTests extends SecurityIntegTestCase { @@ -137,10 +150,21 @@ public void testInvalidHeaders() throws IOException { ) ); } + + try (var ignored = threadContext.stashContext()) { + new CrossClusterAccessHeaders( + getEncodedCrossClusterAccessApiKey(), + AuthenticationTestHelper.randomCrossClusterAccessSubjectInfo() + ).writeToContext(threadContext, createMockSignerWithNoCerts()); + authenticateAndAssertExpectedErrorMessage( + service, + msg -> assertThat(msg, equalTo("Provided signature does not contain any certificates")) + ); + } } - public void testAuthenticateHeadersSuccess() throws IOException { - final String encodedCrossClusterAccessApiKey = getEncodedCrossClusterAccessApiKey(); + public void testAuthenticateHeadersSuccess() throws IOException, CertificateException { + final String encodedCrossClusterAccessApiKey = getEncodedCrossClusterAccessApiKeyWithCertIdentity(); final String nodeName = internalCluster().getRandomNodeName(); final ThreadContext threadContext = internalCluster().getInstance(SecurityContext.class, nodeName).getThreadContext(); final CrossClusterAccessAuthenticationService service = getCrossClusterAccessAuthenticationService(nodeName); @@ -149,7 +173,12 @@ public void testAuthenticateHeadersSuccess() throws IOException { addRandomizedHeaders(threadContext, encodedCrossClusterAccessApiKey); final PlainActionFuture future = new PlainActionFuture<>(); Map headers = withRandomizedAdditionalSecurityHeaders( - Map.of(CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY, encodedCrossClusterAccessApiKey) + Map.of( + CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY, + encodedCrossClusterAccessApiKey, + CROSS_CLUSTER_ACCESS_SIGNATURE_HEADER_KEY, + createTestSignature().encodeToString() + ) ); final ApiKeyService.ApiKeyCredentials credentials = service.extractApiKeyCredentialsFromHeaders(headers); service.tryAuthenticate(credentials, future); @@ -198,10 +227,29 @@ public void testGetApiKeyCredentialsFromHeaders() { ); } + { + ElasticsearchSecurityException ex = expectThrows( + ElasticsearchSecurityException.class, + () -> service.extractApiKeyCredentialsFromHeaders( + withRandomizedAdditionalSecurityHeaders( + Map.of( + CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY, + getEncodedCrossClusterAccessApiKey(), + CROSS_CLUSTER_ACCESS_SIGNATURE_HEADER_KEY, + "not a valid signature" + ) + ) + ) + ); + assertThat(ex.getCause(), instanceOf(IllegalArgumentException.class)); + assertThat(ex.getCause().getMessage(), containsString("Illegal base64 character 20")); + } } - public void testAuthenticateHeadersFailure() throws IOException { - final EncodedKeyWithId encodedCrossClusterAccessApiKeyWithId = getEncodedCrossClusterAccessApiKeyWithId(); + public void testAuthenticateHeadersFailure() throws IOException, CertificateException { + final EncodedKeyWithId encodedCrossClusterAccessApiKeyWithId = getEncodedCrossClusterAccessApiKeyWithId( + new CertificateIdentity("CN=ins*") + ); final EncodedKeyWithId encodedRestApiKeyWithId = getEncodedRestApiKeyWithId(); final String nodeName = internalCluster().getRandomNodeName(); final ThreadContext threadContext = internalCluster().getInstance(SecurityContext.class, nodeName).getThreadContext(); @@ -260,6 +308,22 @@ public void testAuthenticateHeadersFailure() throws IOException { assertThat(actualException.getCause(), instanceOf(ElasticsearchSecurityException.class)); assertThat(actualException.getCause().getMessage(), containsString("unable to find apikey with id")); } + + try (var ignored = threadContext.stashContext()) { + addRandomizedHeaders(threadContext, encodedCrossClusterAccessApiKeyWithId.encoded); + final Map headers = withRandomizedAdditionalSecurityHeaders( + Map.of(CROSS_CLUSTER_ACCESS_CREDENTIALS_HEADER_KEY, encodedCrossClusterAccessApiKeyWithId.encoded) + ); + final ApiKeyService.ApiKeyCredentials credentials = service.extractApiKeyCredentialsFromHeaders(headers); + final PlainActionFuture future = new PlainActionFuture<>(); + service.tryAuthenticate(credentials, future); + final ExecutionException actualException = expectThrows(ExecutionException.class, future::get); + assertThat(actualException.getCause(), instanceOf(ElasticsearchSecurityException.class)); + assertThat( + actualException.getCause().getMessage(), + containsString("Expected signature for cross cluster API key, but no signature was provided") + ); + } } private Map withRandomizedAdditionalSecurityHeaders(Map headers) throws IOException { @@ -283,7 +347,7 @@ private Map withRandomizedAdditionalSecurityHeaders(Map (X509Certificate) cert) + .toArray(X509Certificate[]::new); + } + + private X509CertificateSignature createTestSignature() throws CertificateException, IOException { + return new X509CertificateSignature(getTestCertificates(), "SHA256withRSA", new BytesArray(new byte[] { 1, 2, 3, 4 })); + } + + private CrossClusterApiKeySignatureManager.Signer createMockSigner() throws CertificateException, IOException { + var signer = mock(CrossClusterApiKeySignatureManager.Signer.class); + when(signer.sign(anyString(), anyString())).thenReturn(createTestSignature()); + return signer; + } + + private CrossClusterApiKeySignatureManager.Signer createMockSignerWithNoCerts() { + var signer = mock(CrossClusterApiKeySignatureManager.Signer.class); + when(signer.sign(anyString(), anyString())).thenReturn( + new X509CertificateSignature(new X509Certificate[0], "SHA256withRSA", new BytesArray(new byte[] { 1, 2, 3, 4 })) + ); + return signer; + } + } From 0cc2ea37afd686ccc66bc14230de9881bd7065a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Fred=C3=A9n?= <109296772+jfreden@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:04:33 +0200 Subject: [PATCH 02/22] Update docs/changelog/136299.yaml --- docs/changelog/136299.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/136299.yaml diff --git a/docs/changelog/136299.yaml b/docs/changelog/136299.yaml new file mode 100644 index 0000000000000..852c59db3bc97 --- /dev/null +++ b/docs/changelog/136299.yaml @@ -0,0 +1,5 @@ +pr: 136299 +summary: Validate certificate identity from cross cluster creds +area: Security +type: enhancement +issues: [] From 69c9a172156e27cd0cf5145a72f639c71b919173 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 15 Oct 2025 09:11:29 +0000 Subject: [PATCH 03/22] [CI] Auto commit changes from spotless --- ...RemoteClusterSecurityCrossClusterApiKeySigningIT.java | 1 + .../xpack/security/authc/CrossClusterAccessHeaders.java | 3 ++- .../transport/CrossClusterApiKeySignatureManager.java | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java index e2ad0f67a3bbf..fba76ee9ca494 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.remotecluster; import io.netty.handler.codec.http.HttpMethod; + import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java index 776831496cf34..1439d95f96b41 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java @@ -14,11 +14,12 @@ import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignatureManager; import org.elasticsearch.xpack.security.transport.X509CertificateSignature; -import javax.security.auth.x500.X500Principal; import java.io.IOException; import java.util.Arrays; import java.util.Objects; +import javax.security.auth.x500.X500Principal; + import static org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.CROSS_CLUSTER_ACCESS_SUBJECT_INFO_HEADER_KEY; public final class CrossClusterAccessHeaders { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index 7957919ba03ff..b0b91cf7b30f2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -20,10 +20,6 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.xpack.core.ssl.SslSettingsLoader; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.x500.X500Principal; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -43,6 +39,11 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.KEYSTORE_ALIAS_SUFFIX; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_DIAGNOSE_TRUST; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_SIGNING; From eb7a3f7988067b04a5c30adcb6008f58b4da66d9 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Wed, 15 Oct 2025 11:58:43 +0200 Subject: [PATCH 04/22] fixup! Pattern match --- .../org/elasticsearch/xpack/security/authc/ApiKeyService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 234ab14470ff2..6551dd9f2a57c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1540,7 +1540,7 @@ static void completeApiKeyAuthentication( private static boolean validateCertificateIdentity(String certificateIdentity, String certificateIdentityPattern) { logger.trace("Validating certificate identity [{}] against [{}]", certificateIdentity, certificateIdentityPattern); // Consider adding a cache if this causes performance problems - return Pattern.compile(certificateIdentityPattern).matcher(certificateIdentity).matches(); + return Pattern.matches(certificateIdentityPattern, certificateIdentity); } ApiKeyCredentials parseCredentialsFromApiKeyString(SecureString apiKeyString) { From d63b4ec67f5ce78f37d22388b38c2122cb849e55 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Wed, 15 Oct 2025 16:38:34 +0200 Subject: [PATCH 05/22] Add pattern cache --- .../xpack/security/authc/ApiKeyService.java | 27 ++++++++++++++++--- .../security/authc/ApiKeyServiceTests.java | 15 ++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 6551dd9f2a57c..ceb1981a6e0f3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -208,6 +208,7 @@ public class ApiKeyService implements Closeable { TimeValue.timeValueMinutes(15), Property.NodeScope ); + private static final int MAX_PATTERN_CACHE_SIZE = 100; private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().allowRestriction(true).build(); @@ -219,6 +220,7 @@ public class ApiKeyService implements Closeable { private final boolean enabled; private final Settings settings; private final InactiveApiKeysRemover inactiveApiKeysRemover; + private final Cache certificateIdentityPatternCache; private final Cache> apiKeyAuthCache; private final Hasher cacheHasher; private final ThreadPool threadPool; @@ -271,6 +273,11 @@ public ApiKeyService( .build(); final TimeValue doc_ttl = DOC_CACHE_TTL_SETTING.get(settings); this.apiKeyDocCache = doc_ttl.getNanos() == 0 ? null : new ApiKeyDocCache(doc_ttl, maximumWeight); + this.certificateIdentityPatternCache = CacheBuilder.builder() + .setExpireAfterAccess(ttl) + .setMaximumWeight(MAX_PATTERN_CACHE_SIZE) + .build(); + cacheInvalidatorRegistry.registerCacheInvalidator("api_key", new CacheInvalidatorRegistry.CacheInvalidator() { @Override public void invalidate(Collection keys) { @@ -310,6 +317,7 @@ public void invalidateAll() { } else { this.apiKeyAuthCache = null; this.apiKeyDocCache = null; + this.certificateIdentityPatternCache = null; } if (enabled) { @@ -1472,7 +1480,7 @@ Cache getRoleDescriptorsBytesCache() { } // package-private for testing - static void completeApiKeyAuthentication( + void completeApiKeyAuthentication( ApiKeyDoc apiKeyDoc, ApiKeyCredentials credentials, Clock clock, @@ -1537,10 +1545,21 @@ static void completeApiKeyAuthentication( } } - private static boolean validateCertificateIdentity(String certificateIdentity, String certificateIdentityPattern) { + private boolean validateCertificateIdentity(String certificateIdentity, String certificateIdentityPattern) { logger.trace("Validating certificate identity [{}] against [{}]", certificateIdentity, certificateIdentityPattern); - // Consider adding a cache if this causes performance problems - return Pattern.matches(certificateIdentityPattern, certificateIdentity); + + try { + Pattern pattern = certificateIdentityPatternCache.computeIfAbsent(certificateIdentityPattern, Pattern::compile); + return pattern.matcher(certificateIdentity).matches(); + } catch (ExecutionException e) { + logger.error( + "Failed to validate certificate identity [{}] against pattern [{}] using cache. Falling back to regular matching", + certificateIdentity, + certificateIdentityPattern, + e + ); + return Pattern.matches(certificateIdentityPattern, certificateIdentity); + } } ApiKeyCredentials parseCredentialsFromApiKeyString(SecureString apiKeyString) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 4f17de94c56fb..b927322d7f9b1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -3081,6 +3081,7 @@ public void testAuthenticationFailureWithApiKeyTypeMismatch() throws Exception { } public void testCompleteApiKeyAuthentication() throws IOException { + var apiKeyService = createApiKeyService(); final var apiKeyId = randomAlphaOfLength(12); final var apiKey = randomAlphaOfLength(16); final var hasher = getFastStoredHashAlgoForTests(); @@ -3101,7 +3102,7 @@ public void testCompleteApiKeyAuthentication() throws IOException { final ApiKey.Type expectedType1 = randomValueOtherThan(apiKeyDoc1.type, () -> randomFrom(ApiKey.Type.values())); final ApiKeyCredentials apiKeyCredentials1 = getApiKeyCredentials(apiKeyId, apiKey, expectedType1); final PlainActionFuture> future1 = new PlainActionFuture<>(); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc1, apiKeyCredentials1, clock, future1); + apiKeyService.completeApiKeyAuthentication(apiKeyDoc1, apiKeyCredentials1, clock, future1); final AuthenticationResult auth1 = future1.actionGet(); assertThat(auth1.getStatus(), is(AuthenticationResult.Status.TERMINATE)); assertThat(auth1.getValue(), nullValue()); @@ -3122,7 +3123,7 @@ public void testCompleteApiKeyAuthentication() throws IOException { final var apiKeyDoc2 = buildApiKeyDoc(hash, pastTime, false, -1, randomAlphaOfLengthBetween(3, 8), Version.CURRENT.id); final ApiKeyCredentials apiKeyCredentials2 = getApiKeyCredentials(apiKeyId, apiKey, apiKeyDoc2.type); final PlainActionFuture> future2 = new PlainActionFuture<>(); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc2, apiKeyCredentials2, clock, future2); + apiKeyService.completeApiKeyAuthentication(apiKeyDoc2, apiKeyCredentials2, clock, future2); final AuthenticationResult auth2 = future2.actionGet(); assertThat(auth2.getStatus(), is(AuthenticationResult.Status.CONTINUE)); assertThat(auth2.getValue(), nullValue()); @@ -3139,7 +3140,7 @@ public void testCompleteApiKeyAuthentication() throws IOException { ); final ApiKeyCredentials apiKeyCredentials3 = getApiKeyCredentials(apiKeyId, apiKey, apiKeyDoc3.type); final PlainActionFuture> future3 = new PlainActionFuture<>(); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc3, apiKeyCredentials3, clock, future3); + apiKeyService.completeApiKeyAuthentication(apiKeyDoc3, apiKeyCredentials3, clock, future3); final AuthenticationResult auth3 = future3.actionGet(); assertThat(auth3.getStatus(), is(AuthenticationResult.Status.SUCCESS)); assertThat(auth3.getValue(), notNullValue()); @@ -3337,7 +3338,7 @@ public void testCrossClusterApiKeyCertificateIdentityValidationSuccessful() thro ); final PlainActionFuture> future = new PlainActionFuture<>(); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + createApiKeyService().completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); final AuthenticationResult result = future.get(); assertThat(result, notNullValue()); @@ -3358,7 +3359,7 @@ public void testCrossClusterApiKeyCertificateIdentityValidationNoMatch() throws final PlainActionFuture> future = new PlainActionFuture<>(); final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + createApiKeyService().completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); final AuthenticationResult result = future.get(); assertThat(result, notNullValue()); @@ -3386,7 +3387,7 @@ public void testCrossClusterApiKeyCertificateIdentityValidationNoCertIdentity() final PlainActionFuture> future = new PlainActionFuture<>(); final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); - ApiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentialsWithoutCertIdentity, clock, future); + createApiKeyService().completeApiKeyAuthentication(apiKeyDoc, credentialsWithoutCertIdentity, clock, future); final AuthenticationResult result = future.get(); assertThat(result, notNullValue()); @@ -3454,7 +3455,7 @@ public static Authentication createApiKeyAuthentication( ) ); PlainActionFuture> authenticationResultFuture = new PlainActionFuture<>(); - ApiKeyService.completeApiKeyAuthentication( + apiKeyService.completeApiKeyAuthentication( apiKeyDoc, new ApiKeyService.ApiKeyCredentials("id", new SecureString(randomAlphaOfLength(16).toCharArray()), ApiKey.Type.REST), Clock.systemUTC(), From e598e69be3970e3ae51f3c35db7f6311f951955a Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 16 Oct 2025 11:14:11 +0200 Subject: [PATCH 06/22] Add tests for pattern cache --- .../xpack/security/Security.java | 1 + .../xpack/security/authc/ApiKeyService.java | 46 +++++++----- .../security/authc/ApiKeyServiceTests.java | 72 +++++++++++++++++++ 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 90e8d34b68f7a..ea742a74fc033 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1643,6 +1643,7 @@ public static List> getSettings( settingsList.add(ApiKeyService.CACHE_MAX_KEYS_SETTING); settingsList.add(ApiKeyService.CACHE_TTL_SETTING); settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING); + settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING); settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); settingsList.add(OPERATOR_PRIVILEGES_ENABLED); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index ceb1981a6e0f3..009f128e4da40 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -208,7 +208,12 @@ public class ApiKeyService implements Closeable { TimeValue.timeValueMinutes(15), Property.NodeScope ); - private static final int MAX_PATTERN_CACHE_SIZE = 100; + public static final Setting CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING = Setting.timeSetting( + "xpack.security.authc.api_key.certificate_identity_pattern_cache.ttl", + TimeValue.timeValueHours(48L), + Property.NodeScope + ); + private static final int MAX_PATTERN_CACHE_SIZE = 1000; private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().allowRestriction(true).build(); @@ -271,12 +276,13 @@ public ApiKeyService( .setMaximumWeight(maximumWeight) .removalListener(getAuthCacheRemovalListener(maximumWeight)) .build(); - final TimeValue doc_ttl = DOC_CACHE_TTL_SETTING.get(settings); - this.apiKeyDocCache = doc_ttl.getNanos() == 0 ? null : new ApiKeyDocCache(doc_ttl, maximumWeight); - this.certificateIdentityPatternCache = CacheBuilder.builder() - .setExpireAfterAccess(ttl) - .setMaximumWeight(MAX_PATTERN_CACHE_SIZE) - .build(); + final TimeValue docTtl = DOC_CACHE_TTL_SETTING.get(settings); + this.apiKeyDocCache = docTtl.getNanos() == 0 ? null : new ApiKeyDocCache(docTtl, maximumWeight); + + final TimeValue patternTtl = CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING.get(settings); + this.certificateIdentityPatternCache = patternTtl.getNanos() == 0 + ? null + : CacheBuilder.builder().setExpireAfterAccess(patternTtl).setMaximumWeight(MAX_PATTERN_CACHE_SIZE).build(); cacheInvalidatorRegistry.registerCacheInvalidator("api_key", new CacheInvalidatorRegistry.CacheInvalidator() { @Override @@ -1547,19 +1553,23 @@ void completeApiKeyAuthentication( private boolean validateCertificateIdentity(String certificateIdentity, String certificateIdentityPattern) { logger.trace("Validating certificate identity [{}] against [{}]", certificateIdentity, certificateIdentityPattern); + return getCertificateIdentityPattern(certificateIdentityPattern).matcher(certificateIdentity).matches(); + } - try { - Pattern pattern = certificateIdentityPatternCache.computeIfAbsent(certificateIdentityPattern, Pattern::compile); - return pattern.matcher(certificateIdentity).matches(); - } catch (ExecutionException e) { - logger.error( - "Failed to validate certificate identity [{}] against pattern [{}] using cache. Falling back to regular matching", - certificateIdentity, - certificateIdentityPattern, - e - ); - return Pattern.matches(certificateIdentityPattern, certificateIdentity); + // Visible for testing + Pattern getCertificateIdentityPattern(String certificateIdentityPattern) { + if (certificateIdentityPatternCache != null) { + try { + return certificateIdentityPatternCache.computeIfAbsent(certificateIdentityPattern, Pattern::compile); + } catch (ExecutionException e) { + logger.error( + "Failed to validate certificate identity against pattern [{}] using cache. Falling back to regular matching", + certificateIdentityPattern, + e + ); + } } + return Pattern.compile(certificateIdentityPattern); } ApiKeyCredentials parseCredentialsFromApiKeyString(SecureString apiKeyString) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index b927322d7f9b1..39a3053399ab0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -157,6 +157,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.LongStream; @@ -3395,6 +3396,77 @@ public void testCrossClusterApiKeyCertificateIdentityValidationNoCertIdentity() assertThat(result.getMessage(), equalTo("Expected signature for cross cluster API key, but no signature was provided")); } + public void testPatternCache() throws ExecutionException, InterruptedException { + final String certificateIdentityPattern = "CN=(remote-cluster|test)-.*,OU=engineering,DC=example,DC=com"; + final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); + + final String matchingCertificateIdentity = "CN=remote-cluster-node1,OU=engineering,DC=example,DC=com"; + final ApiKeyCredentials credentials = new ApiKeyCredentials( + randomAlphaOfLength(12), + randomSecureStringOfLength(16), + ApiKey.Type.CROSS_CLUSTER, + matchingCertificateIdentity + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + var apiKeyService = spy(createApiKeyService()); + + final var currentPatternObject = new AtomicReference(); + doAnswer(invocationOnMock -> { + Pattern newPattern = (Pattern) invocationOnMock.callRealMethod(); + if (currentPatternObject.get() != null) { + assertSame(currentPatternObject.get(), newPattern); + } + currentPatternObject.set(newPattern); + return newPattern; + }).when(apiKeyService).getCertificateIdentityPattern(certificateIdentityPattern); + + for (int i = 0; i < randomIntBetween(3, 10); i++) { + apiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + } + } + + public void testPatternCacheDisabled() throws ExecutionException, InterruptedException { + final Settings settings = Settings.builder() + .put( + randomFrom(ApiKeyService.CACHE_TTL_SETTING.getKey(), ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING.getKey()), + "0s" + ) + .build(); + + final String certificateIdentityPattern = "CN=(remote-cluster|test)-.*,OU=engineering,DC=example,DC=com"; + final ApiKeyDoc apiKeyDoc = createCrossClusterApiKeyDocWithCertificateIdentity(certificateIdentityPattern); + + final String matchingCertificateIdentity = "CN=remote-cluster-node1,OU=engineering,DC=example,DC=com"; + final ApiKeyCredentials credentials = new ApiKeyCredentials( + randomAlphaOfLength(12), + randomSecureStringOfLength(16), + ApiKey.Type.CROSS_CLUSTER, + matchingCertificateIdentity + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + var apiKeyService = spy(createApiKeyService(settings)); + + final var currentPatternObject = new AtomicReference(); + doAnswer(invocationOnMock -> { + Pattern newPattern = (Pattern) invocationOnMock.callRealMethod(); + assertNotEquals(currentPatternObject.get(), newPattern); + currentPatternObject.set(newPattern); + return newPattern; + }).when(apiKeyService).getCertificateIdentityPattern(certificateIdentityPattern); + + for (int i = 0; i < randomIntBetween(3, 10); i++) { + apiKeyService.completeApiKeyAuthentication(apiKeyDoc, credentials, clock, future); + final AuthenticationResult result = future.get(); + assertThat(result, notNullValue()); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + } + } + private static RoleDescriptor randomRoleDescriptorWithRemotePrivileges() { return new RoleDescriptor( randomAlphaOfLengthBetween(3, 90), From b4dfd67b28b588757198a3d31291df1cdc4c6270 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 16 Oct 2025 11:25:21 +0200 Subject: [PATCH 07/22] fixup! logging --- .../elasticsearch/xpack/security/authc/ApiKeyService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 009f128e4da40..a6fe7e1a7d743 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1563,8 +1563,10 @@ Pattern getCertificateIdentityPattern(String certificateIdentityPattern) { return certificateIdentityPatternCache.computeIfAbsent(certificateIdentityPattern, Pattern::compile); } catch (ExecutionException e) { logger.error( - "Failed to validate certificate identity against pattern [{}] using cache. Falling back to regular matching", - certificateIdentityPattern, + Strings.format( + "Failed to validate certificate identity against pattern [%s] using cache. Falling back to regular matching", + certificateIdentityPattern + ), e ); } From 59dbd74c141d1009427b54117400f0e46dcb5380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Fred=C3=A9n?= <109296772+jfreden@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:13:13 +0200 Subject: [PATCH 08/22] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Co-authored-by: Tim Vernum --- .../elasticsearch/xpack/security/authc/ApiKeyService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index a6fe7e1a7d743..3dbd200fbdbb4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1780,6 +1780,11 @@ public ApiKey.Type getExpectedType() { return expectedType; } + /** + * The identity (Subject DistinguishedName) of the X.509 certificate that was provided by the client + * alongside the API during authenticate. + * At the time of writing, the only place where this is used is for cross cluster request signing + */ public String getCertificateIdentity() { return certificateIdentity; } From 649a7e481ee810ad0079aafebc2eca9bb9fec102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Fred=C3=A9n?= <109296772+jfreden@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:14:05 +0200 Subject: [PATCH 09/22] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java Co-authored-by: Tim Vernum --- .../org/elasticsearch/xpack/security/authc/ApiKeyService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 3dbd200fbdbb4..5cf52e39f097b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1509,7 +1509,8 @@ void completeApiKeyAuthentication( if (apiKeyDoc.certificateIdentity != null) { if (credentials.getCertificateIdentity() == null) { listener.onResponse( - AuthenticationResult.terminate("Expected signature for cross cluster API key, but no signature was provided") + AuthenticationResult.terminate( + Strings.format("API key (type:[%s], id:[%s]) requires certificate identity [%s], but no certificate was provided", apiKeyDoc.type.value(), credentials.getId(), apiKeyDoc.certificateIdentity)); ); return; } From 4d075354b2c56332e18dbd37379ceb1600bf7351 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Fri, 17 Oct 2025 12:00:21 +0200 Subject: [PATCH 10/22] fixup! Code review comments --- ...sterSecurityCrossClusterApiKeySigningIT.java | 7 ++++++- .../elasticsearch/xpack/security/Security.java | 2 ++ .../xpack/security/authc/ApiKeyService.java | 17 ++++++++++++++--- ...CrossClusterAccessAuthenticationService.java | 8 +++++--- ...erAccessAuthenticationServiceIntegTests.java | 6 +++++- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java index fba76ee9ca494..810f88ce766a8 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java @@ -151,7 +151,12 @@ public void testCrossClusterSearchWithCrossClusterApiKeySigning() throws Excepti .putNull("cluster.remote.my_remote_cluster.signing.key") .build() ); - assertCrossClusterAuthFail("Expected signature for cross cluster API key, but no signature was provided"); + + assertCrossClusterAuthFail( + "API key (type:[cross_cluster], id:[" + + MY_REMOTE_API_KEY_MAP_REF.get().get("id") + + "]) requires certificate identity matching [" + ); // Reset updateClusterSettings( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index ea742a74fc033..7608e72e0c2f0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -472,6 +472,7 @@ import static org.elasticsearch.xpack.core.security.SecurityField.FIELD_LEVEL_SECURITY_FEATURE; import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_VALUE; import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.INCLUDED_RESERVED_ROLES_SETTING; +import static org.elasticsearch.xpack.security.authc.ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_ENABLED; import static org.elasticsearch.xpack.security.transport.SSLEngineUtils.extractClientCertificates; @@ -1644,6 +1645,7 @@ public static List> getSettings( settingsList.add(ApiKeyService.CACHE_TTL_SETTING); settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING); settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING); + settingsList.add(CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); settingsList.add(OPERATOR_PRIVILEGES_ENABLED); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 5cf52e39f097b..7c0746ce7e871 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -213,7 +213,11 @@ public class ApiKeyService implements Closeable { TimeValue.timeValueHours(48L), Property.NodeScope ); - private static final int MAX_PATTERN_CACHE_SIZE = 1000; + public static final Setting CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING = Setting.intSetting( + "xpack.security.authc.api_key.certificate_identity_pattern_cache.max_keys", + 100, + Property.NodeScope + ); private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().allowRestriction(true).build(); @@ -280,9 +284,10 @@ public ApiKeyService( this.apiKeyDocCache = docTtl.getNanos() == 0 ? null : new ApiKeyDocCache(docTtl, maximumWeight); final TimeValue patternTtl = CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING.get(settings); + final int maximumPatternWeight = CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING.get(settings); this.certificateIdentityPatternCache = patternTtl.getNanos() == 0 ? null - : CacheBuilder.builder().setExpireAfterAccess(patternTtl).setMaximumWeight(MAX_PATTERN_CACHE_SIZE).build(); + : CacheBuilder.builder().setExpireAfterAccess(patternTtl).setMaximumWeight(maximumPatternWeight).build(); cacheInvalidatorRegistry.registerCacheInvalidator("api_key", new CacheInvalidatorRegistry.CacheInvalidator() { @Override @@ -1510,7 +1515,13 @@ void completeApiKeyAuthentication( if (credentials.getCertificateIdentity() == null) { listener.onResponse( AuthenticationResult.terminate( - Strings.format("API key (type:[%s], id:[%s]) requires certificate identity [%s], but no certificate was provided", apiKeyDoc.type.value(), credentials.getId(), apiKeyDoc.certificateIdentity)); + Strings.format( + "API key (type:[%s], id:[%s]) requires certificate identity matching [%s], but no certificate was provided", + apiKeyDoc.type.value(), + credentials.getId(), + apiKeyDoc.certificateIdentity + ) + ) ); return; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java index 63075f1337dba..62e59cbbcadbd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java @@ -69,14 +69,16 @@ public void authenticate(final String action, final TransportRequest request, fi try { // parse and add as authentication token as early as possible so that failure events in audit log include API key ID crossClusterAccessHeaders = CrossClusterAccessHeaders.readFromContext(threadContext); + // Extract credentials, including certificate identity from the optional signature without actually verifying the signature final ApiKeyService.ApiKeyCredentials apiKeyCredentials = crossClusterAccessHeaders.credentials(); assert ApiKey.Type.CROSS_CLUSTER == apiKeyCredentials.getExpectedType(); // authn must verify only the provided api key and not try to extract any other credential from the thread context authcContext = authenticationService.newContext(action, request, apiKeyCredentials); - var signature = crossClusterAccessHeaders.signature(); + var signingInfo = crossClusterAccessHeaders.signature(); - // Always validate a signature if provided - if (signature != null && verifySignature(authcContext, signature, crossClusterAccessHeaders, listener) == false) { + // Verify the signing info if provided. The signing info contains both the signature and the certificate identity, but only the + // signature is validated here. The certificate identity is validated later as part of the ApiKeyCredentials validation + if (signingInfo != null && verifySignature(authcContext, signingInfo, crossClusterAccessHeaders, listener) == false) { return; } } catch (Exception ex) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java index 7d7af40726a4d..44030dd07c895 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationServiceIntegTests.java @@ -321,7 +321,11 @@ public void testAuthenticateHeadersFailure() throws IOException, CertificateExce assertThat(actualException.getCause(), instanceOf(ElasticsearchSecurityException.class)); assertThat( actualException.getCause().getMessage(), - containsString("Expected signature for cross cluster API key, but no signature was provided") + containsString( + "API key (type:[cross_cluster], id:[" + + encodedCrossClusterAccessApiKeyWithId.id + + "]) requires certificate identity matching [CN=ins*], but no certificate was provided" + ) ); } } From 2203889b161d3a529f124b762aa0e95a69b6c540 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 17 Oct 2025 10:08:25 +0000 Subject: [PATCH 11/22] [CI] Auto commit changes from spotless --- .../elasticsearch/xpack/security/authc/ApiKeyService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 7c0746ce7e871..7746db56651b4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1793,9 +1793,9 @@ public ApiKey.Type getExpectedType() { } /** - * The identity (Subject DistinguishedName) of the X.509 certificate that was provided by the client - * alongside the API during authenticate. - * At the time of writing, the only place where this is used is for cross cluster request signing + * The identity (Subject DistinguishedName) of the X.509 certificate that was provided by the client + * alongside the API during authenticate. + * At the time of writing, the only place where this is used is for cross cluster request signing */ public String getCertificateIdentity() { return certificateIdentity; From ed4093a08ff3cfe85b2b9f394c93300dc609db14 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Fri, 17 Oct 2025 13:26:21 +0200 Subject: [PATCH 12/22] fixup! CI --- .../xpack/security/authc/ApiKeyServiceTests.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 39a3053399ab0..a453b21b402ea 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -3393,7 +3393,16 @@ public void testCrossClusterApiKeyCertificateIdentityValidationNoCertIdentity() final AuthenticationResult result = future.get(); assertThat(result, notNullValue()); assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.TERMINATE)); - assertThat(result.getMessage(), equalTo("Expected signature for cross cluster API key, but no signature was provided")); + assertThat( + result.getMessage(), + equalTo( + "API key (type:[cross_cluster], id:[" + + credentialsWithoutCertIdentity.getId() + + "]) requires certificate identity matching [" + + certificateIdentityPattern + + "], but no certificate was provided" + ) + ); } public void testPatternCache() throws ExecutionException, InterruptedException { From 094c15f2511d61c3def183c70337c8402f7153b9 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Fri, 17 Oct 2025 13:29:57 +0200 Subject: [PATCH 13/22] fixup! Import --- .../main/java/org/elasticsearch/xpack/security/Security.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 7608e72e0c2f0..b262a6ee65cee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -9,7 +9,6 @@ import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpUtil; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.SetOnce; @@ -472,7 +471,6 @@ import static org.elasticsearch.xpack.core.security.SecurityField.FIELD_LEVEL_SECURITY_FEATURE; import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.INDICES_PERMISSIONS_VALUE; import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.INCLUDED_RESERVED_ROLES_SETTING; -import static org.elasticsearch.xpack.security.authc.ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_ENABLED; import static org.elasticsearch.xpack.security.transport.SSLEngineUtils.extractClientCertificates; @@ -1645,7 +1643,7 @@ public static List> getSettings( settingsList.add(ApiKeyService.CACHE_TTL_SETTING); settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING); settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING); - settingsList.add(CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING); + settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); settingsList.add(OPERATOR_PRIVILEGES_ENABLED); From a71ca11ed100b1f8512f573f176b6f8d0d59588b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 17 Oct 2025 11:37:17 +0000 Subject: [PATCH 14/22] [CI] Auto commit changes from spotless --- .../src/main/java/org/elasticsearch/xpack/security/Security.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index b262a6ee65cee..8199d75a604d0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -9,6 +9,7 @@ import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpUtil; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.SetOnce; From 466badfa454872e43a284e641073e52b5fdb6838 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 21 Oct 2025 09:40:42 +0200 Subject: [PATCH 15/22] fixup! Code review - add test --- ...erSecurityCrossClusterApiKeySigningIT.java | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java index 810f88ce766a8..8df039dbc04eb 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.remotecluster; import io.netty.handler.codec.http.HttpMethod; - import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; @@ -40,6 +39,19 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase { private static final AtomicReference> MY_REMOTE_API_KEY_MAP_REF = new AtomicReference<>(); + private static final String TEST_ACCESS_JSON = """ + { + "search": [ + { + "names": ["index*", "not_found_index"] + } + ] + }"""; + private static final String[] MATCHING_CERTIFICATE_IDENTITY_PATTERNS = new String[] { + "CN=instance", + "^CN=instance$", + "(?i)" + "^CN=instance$", + "^CN=[A-Za-z0-9_]+$" }; static { fulfillingCluster = ElasticsearchCluster.local() @@ -68,19 +80,8 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe .configFile("signing.key", Resource.fromClasspath("signing/signing.key")) .keystore("cluster.remote.my_remote_cluster.credentials", () -> { if (MY_REMOTE_API_KEY_MAP_REF.get() == null) { - final var accessJson = """ - { - "search": [ - { - "names": ["index*", "not_found_index"] - } - ] - }"""; MY_REMOTE_API_KEY_MAP_REF.set( - createCrossClusterAccessApiKey( - accessJson, - randomFrom("CN=instance", "^CN=instance$", "(?i)^CN=instance$", "^CN=[A-Za-z0-9_]+$") - ) + createCrossClusterAccessApiKey(TEST_ACCESS_JSON, randomFrom(MATCHING_CERTIFICATE_IDENTITY_PATTERNS)) ); } return (String) MY_REMOTE_API_KEY_MAP_REF.get().get("encoded"); @@ -169,15 +170,7 @@ public void testCrossClusterSearchWithCrossClusterApiKeySigning() throws Excepti // 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)); + updateCrossClusterAccessApiKey(null); assertCrossClusterSearchSuccessfulWithResult(); // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required @@ -188,6 +181,20 @@ public void testCrossClusterSearchWithCrossClusterApiKeySigning() throws Excepti updateClusterSettingsFulfillingCluster( Settings.builder().put("cluster.remote.signing.certificate_authorities", "signing_ca.crt").build() ); + updateCrossClusterAccessApiKey(randomFrom(MATCHING_CERTIFICATE_IDENTITY_PATTERNS)); + } + + // Test API key with non-matching certificate identity is rejected + { + var nonMatchingCertificateIdentity = randomFrom("", "no-match", "^CN= instance$", "^CN=instance.$", "^cn=instance$"); + updateCrossClusterAccessApiKey(nonMatchingCertificateIdentity); + assertCrossClusterAuthFail( + "DN from provided certificate [CN=instance] does not match API Key certificate identity pattern [" + + nonMatchingCertificateIdentity + + "]" + ); + // Reset + updateCrossClusterAccessApiKey(randomFrom(MATCHING_CERTIFICATE_IDENTITY_PATTERNS)); } } @@ -330,4 +337,25 @@ protected static Map createCrossClusterAccessApiKey(String acces throw new UncheckedIOException(e); } } + + protected static Map updateCrossClusterAccessApiKey(String certificateIdentity) { + initFulfillingClusterClient(); + final var createCrossClusterApiKeyRequest = new Request( + "PUT", + "/_security/cross_cluster/api_key/" + MY_REMOTE_API_KEY_MAP_REF.get().get("id") + ); + createCrossClusterApiKeyRequest.setJsonEntity( + "{\"certificate_identity\": " + (certificateIdentity != null ? "\"" + certificateIdentity + "\"" : "null") + "}" + ); + try { + final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser( + fulfillingClusterClient, + createCrossClusterApiKeyRequest + ); + assertOK(createCrossClusterApiKeyResponse); + return responseAsMap(createCrossClusterApiKeyResponse); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } From 48e5321b9bcc7b29ed66b52c8b733b242f608c67 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 21 Oct 2025 07:47:29 +0000 Subject: [PATCH 16/22] [CI] Auto commit changes from spotless --- .../RemoteClusterSecurityCrossClusterApiKeySigningIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java index 8df039dbc04eb..957546e987719 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.remotecluster; import io.netty.handler.codec.http.HttpMethod; + import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; From f40b0c308e15847f91f966996a74d2d17e0792e9 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 21 Oct 2025 10:01:10 +0200 Subject: [PATCH 17/22] fixup! Code review - add leafCertificate method --- .../CrossClusterApiKeySignatureManagerIntegTests.java | 2 +- .../authc/CrossClusterAccessAuthenticationService.java | 4 ++-- .../xpack/security/authc/CrossClusterAccessHeaders.java | 2 +- .../transport/CrossClusterApiKeySignatureManager.java | 6 +++--- .../xpack/security/transport/X509CertificateSignature.java | 5 +++++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerIntegTests.java index 3e5c57cf3b21a..441cb28c0da90 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerIntegTests.java @@ -45,7 +45,7 @@ public void testSignWithPemKeyConfig() throws GeneralSecurityException { var verifier = manager.verifier(); assertThat(signature.algorithm(), equalToIgnoringCase(keyConfig.getKeys().getFirst().v2().getSigAlgName())); - assertEquals(signature.certificates()[0], keyConfig.getKeys().getFirst().v2()); + assertEquals(signature.leafCertificate(), keyConfig.getKeys().getFirst().v2()); assertTrue(verifier.verify(signature, testHeaders)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java index 62e59cbbcadbd..1d46dcb506f4c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessAuthenticationService.java @@ -149,14 +149,14 @@ private boolean verifySignature( logger.debug(Strings.format("Invalid cross cluster api key signature received [%s]", signature)); authException = Exceptions.authenticationError( "Invalid cross cluster api key signature from [{}]", - X509CertificateSignature.certificateToString(signature.certificates()[0]) + X509CertificateSignature.certificateToString(signature.leafCertificate()) ); } } catch (GeneralSecurityException securityException) { logger.debug(Strings.format("Failed to verify cross cluster api key signature certificate [%s]", signature), securityException); authException = Exceptions.authenticationError( "Failed to verify cross cluster api key signature certificate from [{}]", - X509CertificateSignature.certificateToString(signature.certificates()[0]) + X509CertificateSignature.certificateToString(signature.leafCertificate()) ); } if (authException != null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java index 1439d95f96b41..a0bab548020e3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/CrossClusterAccessHeaders.java @@ -95,7 +95,7 @@ public static String getCertificateIdentity(X509CertificateSignature signature) if (signature.certificates().length == 0) { throw new IllegalArgumentException("Provided signature does not contain any certificates"); } - return signature.certificates()[0].getSubjectX500Principal().getName(X500Principal.RFC2253); + return signature.leafCertificate().getSubjectX500Principal().getName(X500Principal.RFC2253); } return null; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index b0b91cf7b30f2..f9ccbe440c475 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -178,7 +178,7 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr } // Make sure the provided certificate chain is trusted - var leaf = signature.certificates()[0]; + var leaf = signature.leafCertificate(); if (logger.isTraceEnabled()) { logger.trace( "checking signing chain (len={}) [{}] with leaf subject [{}] using algorithm [{}]", @@ -190,10 +190,10 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr leaf.getPublicKey().getAlgorithm() ); } - authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm()); + authTrustManager.checkClientTrusted(signature.certificates(), leaf.getPublicKey().getAlgorithm()); final Signature signer = Signature.getInstance(signature.algorithm()); - signer.initVerify(signature.certificates()[0]); + signer.initVerify(leaf); signer.update(getSignableBytes(headers)); return signer.verify(signature.signature().array()); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java index 1e80239defa66..085630c5f17a9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java @@ -74,6 +74,11 @@ public X509Certificate[] certificates() { return certificateChain; } + public X509Certificate leafCertificate() { + assert certificateChain.length > 0; + return certificateChain[0]; + } + public String algorithm() { return algorithm; } From 8a16d0879259171ed070ce7018755d976e663f89 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 21 Oct 2025 10:49:18 +0200 Subject: [PATCH 18/22] Add code + test to make sure provided certs are not expired --- .../CrossClusterApiKeySignatureManager.java | 13 +++++---- ...ossClusterApiKeySignatureManagerTests.java | 23 ++++++++++++++++ .../xpack/security/signature/expired_cert.crt | 18 +++++++++++++ .../xpack/security/signature/expired_key.key | 27 +++++++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_cert.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_key.key diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index f9ccbe440c475..3f596bc50f1db 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -20,6 +20,10 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.xpack.core.ssl.SslSettingsLoader; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -39,11 +43,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.x500.X500Principal; - import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.KEYSTORE_ALIAS_SUFFIX; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_DIAGNOSE_TRUST; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_SIGNING; @@ -190,6 +189,10 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr leaf.getPublicKey().getAlgorithm() ); } + for (var certificate : signature.certificates()) { + certificate.checkValidity(); + } + authTrustManager.checkClientTrusted(signature.certificates(), leaf.getPublicKey().getAlgorithm()); final Signature signer = Signature.getInstance(signature.algorithm()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java index 125cb9f82aa59..cf3c724e755ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java @@ -17,6 +17,7 @@ import org.junit.After; import java.security.GeneralSecurityException; +import java.security.cert.CertificateException; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsString; @@ -206,6 +207,28 @@ public void testSignAndVerifyFailsIntermediateCertMissing() { ); } + public void testSignAndVerifyExpiredCertFails() { + var builder = Settings.builder() + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + + builder.put( + "cluster.remote.signing.certificate_authorities", + getDataPath("/org/elasticsearch/xpack/security/signature/expired_cert.crt") + ) + .put( + "cluster.remote.my_remote.signing.certificate", + getDataPath("/org/elasticsearch/xpack/security/signature/expired_cert.crt") + ) + .put("cluster.remote.my_remote.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/expired_key.key")); + + var manager = new CrossClusterApiKeySignatureManager(TestEnvironment.newEnvironment(builder.build())); + var signature = manager.signerForClusterAlias("my_remote").sign("a_header"); + var verifier = manager.verifier(); + var exception = assertThrows(CertificateException.class, () -> verifier.verify(signature, "test")); + assertThat(exception.getMessage(), containsString(inFipsJvm() ? "certificate expired on" : "NotAfter")); + } + private void addStorePathToBuilder(String storeName, String password, String passwordFips, Settings.Builder builder) { String storeType = inFipsJvm() ? "BCFKS" : "PKCS12"; String extension = inFipsJvm() ? ".bcfks" : ".jks"; diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_cert.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_cert.crt new file mode 100644 index 0000000000000..4cb61f13bddf0 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_cert.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+DCCAeCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRleHBp +cmVkLmV4YW1wbGUudGVzdDAeFw0yMjAxMDEwMDAwMDBaFw0yMjAxMDIwMDAwMDBa +MB8xHTAbBgNVBAMMFGV4cGlyZWQuZXhhbXBsZS50ZXN0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAiX2ySZHFNlkfQ8IbZAThs0dLLWOIPYgnkdRja3ka +K+T+wUJ4jkNYbNGkSDKtTZYg5aNpjPZHU0ZnG2V0oA0zYPJOnsBYj7B+CSRhGctC +i2KVbvYBPJ0b0w6dgp5UACJJTeUedE8oFAiW9jbaXvtXXpbtPGo2JE8qO271Bb3Z +OO9UuB9BKwgUOaXzzwXIg/JG4M3ngftbXkdHPBAWCQa4OjB5YYJMoIJEQmQXQTXT +xGUWqM0mAJeiXS0cSxYDO3Tie4gbg0qHy/xPXnwtTmRW8lj5JJWYQ0SYSHjC4aEX +rjlkkS6YlU7/PBseXhRzHDrfL10X+IH+Eoa62i3QyBV32QIDAQABoz8wPTAMBgNV +HRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI +KwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAGg9f3L3pXpT867fSVyTQHP1mdN4 +t0aViKfU2qBgAwasbid8eYR2sv3I0+pKbWraAk3c6uDsYQFvkx0l89e28kCnyLK7 +nJmSvsSPKDbU1eWrylgxyauJL6iTNbwd7ApcWatwFU4Fp3Vql3KPApU73lITznXu +i4ONbwsz1tSp3SM36Yz3umj7+xH/V/m7ofmY8us/+wyGURvUT9AjuLSK7KfpyIGV +ji7Qn0O1KIMfF6foPv4dY23UsT+cXLFkMNgRwRT3HtB0NAAqzZkbVlY1cjU3f7j1 +YTM1ETlhUr/EMPHkFkzCbF5LZqKm+jrwvFkg//68YsDOjSR37ZW4If6m4fs= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_key.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_key.key new file mode 100644 index 0000000000000..a5ea11cab6eed --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_key.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAiX2ySZHFNlkfQ8IbZAThs0dLLWOIPYgnkdRja3kaK+T+wUJ4 +jkNYbNGkSDKtTZYg5aNpjPZHU0ZnG2V0oA0zYPJOnsBYj7B+CSRhGctCi2KVbvYB +PJ0b0w6dgp5UACJJTeUedE8oFAiW9jbaXvtXXpbtPGo2JE8qO271Bb3ZOO9UuB9B +KwgUOaXzzwXIg/JG4M3ngftbXkdHPBAWCQa4OjB5YYJMoIJEQmQXQTXTxGUWqM0m +AJeiXS0cSxYDO3Tie4gbg0qHy/xPXnwtTmRW8lj5JJWYQ0SYSHjC4aEXrjlkkS6Y +lU7/PBseXhRzHDrfL10X+IH+Eoa62i3QyBV32QIDAQABAoIBAAv1JGEdHil+oUc3 +lcHCYe3BZNKm/iWZ1pUqg1YIw4OvSA+/S/RVe5aFOhQ0YhmmFajbKDYkAiqGLDf4 +ACdeVopXMIrCgEFI8IEzioVfheNQ+N2cZBmlMVMPCZhfVD9pu/XcGCS18K6jW5vA +xAewNNZLaokPVLOeNfZSzVkJPJ/kohbNZnZ/7LJpPhkQMS9E/Epsc4ssuWock2+M +8BzBe5ojBQq4b0RHzVX5eTCEcU1fUFRhNdPVHS6ge2b9HVYvgkfOdZAxTPvS6drz +XdLhOB6ejJtzuwV4H2WdQD9FxCbAHQoc4mfsePIJY7xIOp8E7XLYTMjwEE4y51hE +Z0g3Y2ECgYEAvmB9vSVdnFlZ4hTMsFNpcJKrfHLV1VjKODqDlHmvET9QAtRQuPZf +w/QGFFgQWdF9MA9kdTA8sGpWB3F4ocTsJoSjcHC6JZcCoYGq/EwMzJTRULGy5CS3 +T2Iw4L1FyR+TvV/pMZ/e3KlNb3G5J9nL5AcKmHlR7Uj+gWDw8YijhgkCgYEAuOJf +G/uWczwjZiQpp8J8qaRewxAfZmMAGuQz++isLRDsmrgcO11fi7DRDUZtZ0XYllFM +HaxCD9ylKG8ZkLMHgZhthD+hXVEhQ1UCSvhBvBH5/9/SNcQ+zZlqJd8kqS6kZ6mO +DNXvpwlRLhQ7GEVLa/KJmhvsPtADSZ+yGJYzV1ECgYBEcFvDi5NDibuOUvvix5m4 +JoIPkxjcQbcwz5hN4BDXmnMhEUKTu75hq3AkECSQpYbjqCTtHBI1OYUD1/8N6MUc +c8JtwZipusfvV7YoTQ6s3TMCV1+ANNLRbvMzV9emnft2sOwcMuTjXtm/npp5gQqT +RxTAC50wOHWF/Vug+QYQQQKBgB8qyi3Fc89pS+5kGkGVn75ZX0Ay75grCFGANuM4 +jg3q1HggoWHAk+LQo+1A+iB2vdRZUCOQbnKOF6mBUSX7iEBomc25jFJW4uye4AwT +XcF+pk5kROObeZrfSe2oeF2s5zoycyfmyMe34l+KOYwV4mkIMfowytqr5mH68eMD +LxSBAoGACulgQVRArrpb1c4YZus5FNvLo791JlRsP+w/4zF+AVLahsae2l0u3iHg +LKOji1DaGBU6X/wSue7a2PQd4jDUOuJHjtr15+jf1x7eVko10z/gFDqqxc2dfqTb +ZqJHcHZdg8o7vhSUZRVSuYz4oAIOsCB/DL2UDXa5JRSy+6aBLRU= +-----END RSA PRIVATE KEY----- From 039e8ba9717544d9aad59a5b1ce301c8ecc523f6 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 21 Oct 2025 08:54:58 +0000 Subject: [PATCH 19/22] [CI] Auto commit changes from spotless --- .../transport/CrossClusterApiKeySignatureManager.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index 3f596bc50f1db..14025cc9c78f6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -20,10 +20,6 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.xpack.core.ssl.SslSettingsLoader; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.x500.X500Principal; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -43,6 +39,11 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.KEYSTORE_ALIAS_SUFFIX; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_DIAGNOSE_TRUST; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningSettings.SETTINGS_PART_SIGNING; From aec1249b21a3b4b780dcd10da378f5ca64cc023d Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 21 Oct 2025 13:26:09 +0200 Subject: [PATCH 20/22] fixup! Remove duplicate create call --- .../security/transport/CrossClusterApiKeySignatureManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index 14025cc9c78f6..5c15e451afac2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -77,7 +77,6 @@ public void reload(Settings settings) { true ) ? wrapInDiagnosticTrustManager(trustConfig.createTrustManager()) : trustConfig.createTrustManager(); - trustConfig.createTrustManager(); if (newTrustManager.getAcceptedIssuers().length == 0) { logger.warn("Cross cluster API Key trust configuration [{}] has no accepted certificate issuers", trustConfig); trustManager.set(null); From 566770ebf5b2f8d5268c078ac5ea06830c2e3793 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Wed, 22 Oct 2025 14:59:10 +0200 Subject: [PATCH 21/22] fixup! Validate exipry trust anchor --- .../CrossClusterApiKeySignatureManager.java | 45 ++++++++++++++++++- .../transport/X509CertificateSignature.java | 4 ++ ...ossClusterApiKeySignatureManagerTests.java | 25 +++++++++++ .../security/signature/expired_ca_cert.crt | 20 +++++++++ .../signature/valid_cert_with_expired_ca.crt | 23 ++++++++++ .../signature/valid_key_with_expired_ca.key | 27 +++++++++++ 6 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index 5c15e451afac2..efce03435748e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -189,17 +189,60 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr leaf.getPublicKey().getAlgorithm() ); } + + authTrustManager.checkClientTrusted(signature.certificates(), leaf.getPublicKey().getAlgorithm()); + for (var certificate : signature.certificates()) { certificate.checkValidity(); } - authTrustManager.checkClientTrusted(signature.certificates(), leaf.getPublicKey().getAlgorithm()); + var trustAnchor = findTrustAnchor(signature.topCertificate(), trustManager.get().getAcceptedIssuers()); + assert trustAnchor != null : Strings.format("Failed to find trust anchor for [%s]", signature.topCertificate()); + + trustAnchor.checkValidity(); final Signature signer = Signature.getInstance(signature.algorithm()); signer.initVerify(leaf); signer.update(getSignableBytes(headers)); return signer.verify(signature.signature().array()); } + + /** + * Find the certificate that issued the certificate at the top of the current chain. A certificate chain is a list of + * certificates followed by one or more CA certificates (usually the last one being a self-signed certificate), with the + * following properties: + *
+ * 1. The Issuer of each certificate (except the last one) matches the Subject of the next certificate in the list. + * 2. Each certificate (except the last one) is signed by the secret key corresponding to the next certificate in the chain (i.e. + * the signature of one certificate can be verified using the public key contained in the following certificate). + * 3. The last certificate in the list is a trust anchor. + */ + private X509Certificate findTrustAnchor(X509Certificate topCert, X509Certificate[] trustAnchors) { + X500Principal issuer = topCert.getIssuerX500Principal(); + + for (X509Certificate anchor : trustAnchors) { + // Check if the top cert itself is the trust anchor (handles directly trusted certs and full chains) + // Per X.509 spec, a certificate is uniquely identified by issuer DN + serial number + if (anchor.getIssuerX500Principal().equals(topCert.getIssuerX500Principal()) + && anchor.getSerialNumber().equals(topCert.getSerialNumber())) { + return anchor; + } + + if (anchor.getSubjectX500Principal().equals(issuer)) { + try { + // Verify this anchor actually signed the top cert + topCert.verify(anchor.getPublicKey()); + return anchor; + } catch (GeneralSecurityException e) { + logger.trace( + "Trust anchor [{}] matches issuer name but did not sign the certificate", + anchor.getSubjectX500Principal() + ); + } + } + } + return null; + } } public class Signer { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java index 085630c5f17a9..bb54a27a32359 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java @@ -79,6 +79,10 @@ public X509Certificate leafCertificate() { return certificateChain[0]; } + public X509Certificate topCertificate() { + return certificateChain[certificateChain.length - 1]; + } + public String algorithm() { return algorithm; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java index cf3c724e755ed..1792e1dad611e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java @@ -229,6 +229,31 @@ public void testSignAndVerifyExpiredCertFails() { assertThat(exception.getMessage(), containsString(inFipsJvm() ? "certificate expired on" : "NotAfter")); } + public void testSignAndVerifyExpiredTrustAnchorCertFails() { + var builder = Settings.builder() + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + + builder.put( + "cluster.remote.signing.certificate_authorities", + getDataPath("/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt") + ) + .put( + "cluster.remote.my_remote.signing.certificate", + getDataPath("/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt") + ) + .put( + "cluster.remote.my_remote.signing.key", + getDataPath("/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key") + ); + + var manager = new CrossClusterApiKeySignatureManager(TestEnvironment.newEnvironment(builder.build())); + var signature = manager.signerForClusterAlias("my_remote").sign("a_header"); + var verifier = manager.verifier(); + var exception = assertThrows(CertificateException.class, () -> verifier.verify(signature, "test")); + assertThat(exception.getMessage(), containsString(inFipsJvm() ? "certificate expired on" : "NotAfter")); + } + private void addStorePathToBuilder(String storeName, String password, String passwordFips, Settings.Builder builder) { String storeType = inFipsJvm() ? "BCFKS" : "PKCS12"; String extension = inFipsJvm() ? ".bcfks" : ".jks"; diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt new file mode 100644 index 0000000000000..bb643913b6cc6 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIUFZpIskODPIOEFLzG1VL/Oqes+g0wDQYJKoZIhvcNAQEL +BQAwPTEYMBYGA1UEAwwPRXhwaXJlZCBUZXN0IENBMRQwEgYDVQQKDAtUZXN0IENB +IE9yZzELMAkGA1UEBhMCVVMwHhcNMDAwMTAxMDAwMDAwWhcNMTAwMTAxMDAwMDAw +WjA9MRgwFgYDVQQDDA9FeHBpcmVkIFRlc3QgQ0ExFDASBgNVBAoMC1Rlc3QgQ0Eg +T3JnMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMDDDRT20V+krmukCsLk2uGRA49qWPaXaMLnchJYSGvy9FAh7ziqcjlG2d5MiBzf +GGB4GBK6zsysYxoaUeJxDSRtFEsSXy4iMyDRU0FmUM3ORhymLdQYZXRC/wWnzruc +PCwsfqUbnBvOqOLHHIboWxkvp3HZokqioiBG4VZuTMlxMvpIklHlw8JmQnL+35+a +TFlJYSozh2Kld3g0g1UEwESMoO4t2xDwFO2O73wk/zryeBmPpKkt9CS2Nc759fEV +VFbXZmcbJO6+WwxZEQS4rrX8vI7eKs/TPGQyeMevJZi0PYnZqP601Lx0y+4y/PiN +JqtrdGSRdNklSlhHET3R0c8CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFIIJuktlwdkN4v6sgRZSql7X4iliMA0GCSqG +SIb3DQEBCwUAA4IBAQBwvmoOdFyjHUx8V99zcgd8HOQ/XB7Uctt9te/NfLlhVCN0 +ek0feLWmMJr9XqYM1CJ9jVG78ZS56FitwHkXWRmTqr69+WaZRTrW5csNroyICBS4 +BHhbOM7iEVRenVyGVYGadhBYVAVn/9mkRVARQiIUheNK/3v9pyMuSLf4Sd5QYs6p +svXin434/PxFj3Teoplqv+iXsPwy7QU5jq+KXl0PWDkxc3Ku7tgc/1Nsk5Zlf3ck +pQNsdZK979Zx+aTLIbkZnYojgqJSO+2nwTTQj5xZziK9ckeAHvD7VIH5hMDmwAzf +bjXnaZG0Bfb6vE67G0STGWqNto7M6sXMgL6UgLgm +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt new file mode 100644 index 0000000000000..6e03e79b22f7b --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIUOLClxkGLN1IzdCtlAzyv7ubfnGswDQYJKoZIhvcNAQEL +BQAwPTEYMBYGA1UEAwwPRXhwaXJlZCBUZXN0IENBMRQwEgYDVQQKDAtUZXN0IENB +IE9yZzELMAkGA1UEBhMCVVMwIBcNMjUxMDIyMDkyNDIwWhgPMjEyNTA5MjgwOTI5 +MjBaMEIxIDAeBgNVBAMMF3ZhbGlkLWxlYWYuZXhhbXBsZS50ZXN0MREwDwYDVQQK +DAhMZWFmIE9yZzELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCesxBHTVrGTnbZ7afvsHEE1xpVDdA8eZgOakFa6JJW4zwdRGIfUbw/ +yWTBiZpq2I1Xi7dbY6ReH09UtbWUPCXrhZ71Vq4MO2FSd1knZqXIoqjX9jRAGT5J ++WU3tPNL2aN7fGSQRHKR9mO/TYhTf9yQGCRHzzpLNHJ1RXOKEkdmhsZZ36wz0LKW +Ps01gzFsTnp8T4SpttJryW2f74q0ZJvT2oSynqyuJS5MbSwqHl2nyeao8Z8BNVe3 +EOKQAWHUUzpf1xy0/p6Eb4LQObxl9N8sJ3zHX/CcwMpcgh/NRsbQn+TiKJwVkGa3 +SU+UxKlCD1+rTkIr//J91njgictfoXnbAgMBAAGjgbUwgbIwDAYDVR0TAQH/BAIw +ADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MDMGA1UdEQQsMCqCF3ZhbGlkLWxlYWYuZXhhbXBsZS50ZXN0gglsb2NhbGhvc3SH +BH8AAAEwHwYDVR0jBBgwFoAUggm6S2XB2Q3i/qyBFlKqXtfiKWIwHQYDVR0OBBYE +FEqSu+i1sHYHANJN5DzvUCZYo6hhMA0GCSqGSIb3DQEBCwUAA4IBAQCDYdk9rdz9 +/q4WvH3FwCA5BwFuiaglL8w0k0DhRCuJBd94YdnmMqNbf8C4elJJF6z1nH6Fv2+9 +2KFQ26VsTchGSDrTy+GGT9F7xMQl/pXAvH0aQs/Km1egtWV6GKb0z1HPqyni0Xet +swJZ6JKlArKjUKGzKCq7fKBm6BR9xvPj5K2wsfRAozUUFfaOou/jBZ0PDpAZoyZb +MDMh0EfcvKsOh2VesMsfK3bwXbCScjmkSsIebTC+jWHKpoEm2SlVHAMJRSRWIVV1 +AgKMYcwLQRNcslkS7I6HtEjF79vhR54Yu5kxSuU9QiGgYmccfUrzxikg5M+s7nQm +zbQhWJ68ME07 +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key new file mode 100644 index 0000000000000..a5070c25188cc --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnrMQR01axk522e2n77BxBNcaVQ3QPHmYDmpBWuiSVuM8HURi +H1G8P8lkwYmaatiNV4u3W2OkXh9PVLW1lDwl64We9VauDDthUndZJ2alyKKo1/Y0 +QBk+SfllN7TzS9mje3xkkERykfZjv02IU3/ckBgkR886SzRydUVzihJHZobGWd+s +M9Cylj7NNYMxbE56fE+EqbbSa8ltn++KtGSb09qEsp6sriUuTG0sKh5dp8nmqPGf +ATVXtxDikAFh1FM6X9cctP6ehG+C0Dm8ZfTfLCd8x1/wnMDKXIIfzUbG0J/k4iic +FZBmt0lPlMSpQg9fq05CK//yfdZ44InLX6F52wIDAQABAoIBACuB7dGObHd3ZXAD +jonQtntcOVTeD5u0vjIdgUNaBVyauY1QhRIPB2v5W40Pm2z1Z3J71E3SuGoxbT9M +/bXg84hpPpYGKHskAF9qZt+9bW/e4Cksz1BPW1tOayhljFncFcyx4qQGj95iTSBS +MjCqGh7K749cSZ/6hfKOkslkj3yUwdlA8I27MO4iDMhPj6z+eubHV2G3ypfefGlD +QQ7ljuFodobtR7udUVWYnPhuScJyUvHG//uCMaLLaW2rWXD3jv0MUbIfTdYUetig +p0swMeZGUmGh1zlvlJ+l2LBJMXeEi6gzWsHaRO6JJHw/nq64nVb8mq7dCiljRVGv +G8THEFkCgYEAzoS7zVl1A4M/DbJLJ7i/ZWlT48McAizYx9oq1eAgObrdmDe2Jbvm +TIwgS1RUfjfViRwgWbuR0X7xt9OIT2QnDGqkOnDJzu764XtLT/D4TNJA1QG3H4L2 +Jy0JksMLhoUXIormGhAV0onSSfNV5SqHt//OrDAQKwfCIUXh9+62LbcCgYEAxLk+ +sEtHp9EzUGYayUbXghWk1u3/RiuEtlx2sS3v+qtbOGAfyXzPYAHJVFiHFo2/JG1Q +rsYVx5rj/YoQwiFHjR0lkLeEpRqhXv1s1goaUiuBVxzmHX8V97QPLfzEuQLabZwM +9pWLH8zGTKrSIooRb4I1HU7ooElVYDX02JYcFP0CgYEAiJc953ntbN9XyuVL0//b +h2V8uL4JPl8PGk/v2PmeFtDDU7Q1Ywu+LI7ZpTknkTu4njDeLLtknJ1LnnvoQipJ +sWqvKIAE2jsx8ASuMTd94sGFY9z4k3z49bxSAqHCc7x/KreXrVFKPbAuR/8LpsDU +dxxYQ4aeivdcrMkdxfA6yk0CgYAEhgn1/dUo+7uFVsO46yMbf6nps1FSaL/FfbzQ ++DBzgCs50aQJexA9sezSPrLkht/lU4ouaqmnjF0/wEQAYsmFai0p9b5cGY+qYoN1 +LIhMaWmw+h4kgX6c0owiz5QqePFS4eq+ZNPtKEVLEAaC+s/J06GrCdx5ixYmfzch +H9qHdQKBgAvOPI7f8bLNJManzHfcnnNnxrKYg1J3YNvRJa+QkGR3sXQULT2NRiT/ +35NrOnKo0pwCgVMgO2zkSG3jQMCG9Yd9hE5fRHCwjhMUK5i7R4xS5FwrvowO+IMW +g0yyx7tg4keTdlKo9KuJSVlv+tnRIX8ZmYxBMcdtIbLEoMTbA4Yh +-----END RSA PRIVATE KEY----- From 897f87a774a31b8f79eda5b5276abc1c1529d503 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 23 Oct 2025 09:09:51 +0200 Subject: [PATCH 22/22] fixup! Remove trust anchor validit check --- .../CrossClusterApiKeySignatureManager.java | 47 +------------------ ...ossClusterApiKeySignatureManagerTests.java | 25 ---------- .../security/signature/expired_ca_cert.crt | 20 -------- .../signature/valid_cert_with_expired_ca.crt | 23 --------- .../signature/valid_key_with_expired_ca.key | 27 ----------- 5 files changed, 1 insertion(+), 141 deletions(-) delete mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt delete mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt delete mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java index efce03435748e..88f5535ae4d14 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManager.java @@ -191,58 +191,13 @@ public boolean verify(X509CertificateSignature signature, String... headers) thr } authTrustManager.checkClientTrusted(signature.certificates(), leaf.getPublicKey().getAlgorithm()); - - for (var certificate : signature.certificates()) { - certificate.checkValidity(); - } - - var trustAnchor = findTrustAnchor(signature.topCertificate(), trustManager.get().getAcceptedIssuers()); - assert trustAnchor != null : Strings.format("Failed to find trust anchor for [%s]", signature.topCertificate()); - - trustAnchor.checkValidity(); + signature.leafCertificate().checkValidity(); final Signature signer = Signature.getInstance(signature.algorithm()); signer.initVerify(leaf); signer.update(getSignableBytes(headers)); return signer.verify(signature.signature().array()); } - - /** - * Find the certificate that issued the certificate at the top of the current chain. A certificate chain is a list of - * certificates followed by one or more CA certificates (usually the last one being a self-signed certificate), with the - * following properties: - *
- * 1. The Issuer of each certificate (except the last one) matches the Subject of the next certificate in the list. - * 2. Each certificate (except the last one) is signed by the secret key corresponding to the next certificate in the chain (i.e. - * the signature of one certificate can be verified using the public key contained in the following certificate). - * 3. The last certificate in the list is a trust anchor. - */ - private X509Certificate findTrustAnchor(X509Certificate topCert, X509Certificate[] trustAnchors) { - X500Principal issuer = topCert.getIssuerX500Principal(); - - for (X509Certificate anchor : trustAnchors) { - // Check if the top cert itself is the trust anchor (handles directly trusted certs and full chains) - // Per X.509 spec, a certificate is uniquely identified by issuer DN + serial number - if (anchor.getIssuerX500Principal().equals(topCert.getIssuerX500Principal()) - && anchor.getSerialNumber().equals(topCert.getSerialNumber())) { - return anchor; - } - - if (anchor.getSubjectX500Principal().equals(issuer)) { - try { - // Verify this anchor actually signed the top cert - topCert.verify(anchor.getPublicKey()); - return anchor; - } catch (GeneralSecurityException e) { - logger.trace( - "Trust anchor [{}] matches issuer name but did not sign the certificate", - anchor.getSubjectX500Principal() - ); - } - } - } - return null; - } } public class Signer { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java index 1792e1dad611e..cf3c724e755ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerTests.java @@ -229,31 +229,6 @@ public void testSignAndVerifyExpiredCertFails() { assertThat(exception.getMessage(), containsString(inFipsJvm() ? "certificate expired on" : "NotAfter")); } - public void testSignAndVerifyExpiredTrustAnchorCertFails() { - var builder = Settings.builder() - .put("path.home", createTempDir()) - .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); - - builder.put( - "cluster.remote.signing.certificate_authorities", - getDataPath("/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt") - ) - .put( - "cluster.remote.my_remote.signing.certificate", - getDataPath("/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt") - ) - .put( - "cluster.remote.my_remote.signing.key", - getDataPath("/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key") - ); - - var manager = new CrossClusterApiKeySignatureManager(TestEnvironment.newEnvironment(builder.build())); - var signature = manager.signerForClusterAlias("my_remote").sign("a_header"); - var verifier = manager.verifier(); - var exception = assertThrows(CertificateException.class, () -> verifier.verify(signature, "test")); - assertThat(exception.getMessage(), containsString(inFipsJvm() ? "certificate expired on" : "NotAfter")); - } - private void addStorePathToBuilder(String storeName, String password, String passwordFips, Settings.Builder builder) { String storeType = inFipsJvm() ? "BCFKS" : "PKCS12"; String extension = inFipsJvm() ? ".bcfks" : ".jks"; diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt deleted file mode 100644 index bb643913b6cc6..0000000000000 --- a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/expired_ca_cert.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIUFZpIskODPIOEFLzG1VL/Oqes+g0wDQYJKoZIhvcNAQEL -BQAwPTEYMBYGA1UEAwwPRXhwaXJlZCBUZXN0IENBMRQwEgYDVQQKDAtUZXN0IENB -IE9yZzELMAkGA1UEBhMCVVMwHhcNMDAwMTAxMDAwMDAwWhcNMTAwMTAxMDAwMDAw -WjA9MRgwFgYDVQQDDA9FeHBpcmVkIFRlc3QgQ0ExFDASBgNVBAoMC1Rlc3QgQ0Eg -T3JnMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMDDDRT20V+krmukCsLk2uGRA49qWPaXaMLnchJYSGvy9FAh7ziqcjlG2d5MiBzf -GGB4GBK6zsysYxoaUeJxDSRtFEsSXy4iMyDRU0FmUM3ORhymLdQYZXRC/wWnzruc -PCwsfqUbnBvOqOLHHIboWxkvp3HZokqioiBG4VZuTMlxMvpIklHlw8JmQnL+35+a -TFlJYSozh2Kld3g0g1UEwESMoO4t2xDwFO2O73wk/zryeBmPpKkt9CS2Nc759fEV -VFbXZmcbJO6+WwxZEQS4rrX8vI7eKs/TPGQyeMevJZi0PYnZqP601Lx0y+4y/PiN -JqtrdGSRdNklSlhHET3R0c8CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFIIJuktlwdkN4v6sgRZSql7X4iliMA0GCSqG -SIb3DQEBCwUAA4IBAQBwvmoOdFyjHUx8V99zcgd8HOQ/XB7Uctt9te/NfLlhVCN0 -ek0feLWmMJr9XqYM1CJ9jVG78ZS56FitwHkXWRmTqr69+WaZRTrW5csNroyICBS4 -BHhbOM7iEVRenVyGVYGadhBYVAVn/9mkRVARQiIUheNK/3v9pyMuSLf4Sd5QYs6p -svXin434/PxFj3Teoplqv+iXsPwy7QU5jq+KXl0PWDkxc3Ku7tgc/1Nsk5Zlf3ck -pQNsdZK979Zx+aTLIbkZnYojgqJSO+2nwTTQj5xZziK9ckeAHvD7VIH5hMDmwAzf -bjXnaZG0Bfb6vE67G0STGWqNto7M6sXMgL6UgLgm ------END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt deleted file mode 100644 index 6e03e79b22f7b..0000000000000 --- a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_cert_with_expired_ca.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIUOLClxkGLN1IzdCtlAzyv7ubfnGswDQYJKoZIhvcNAQEL -BQAwPTEYMBYGA1UEAwwPRXhwaXJlZCBUZXN0IENBMRQwEgYDVQQKDAtUZXN0IENB -IE9yZzELMAkGA1UEBhMCVVMwIBcNMjUxMDIyMDkyNDIwWhgPMjEyNTA5MjgwOTI5 -MjBaMEIxIDAeBgNVBAMMF3ZhbGlkLWxlYWYuZXhhbXBsZS50ZXN0MREwDwYDVQQK -DAhMZWFmIE9yZzELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQCesxBHTVrGTnbZ7afvsHEE1xpVDdA8eZgOakFa6JJW4zwdRGIfUbw/ -yWTBiZpq2I1Xi7dbY6ReH09UtbWUPCXrhZ71Vq4MO2FSd1knZqXIoqjX9jRAGT5J -+WU3tPNL2aN7fGSQRHKR9mO/TYhTf9yQGCRHzzpLNHJ1RXOKEkdmhsZZ36wz0LKW -Ps01gzFsTnp8T4SpttJryW2f74q0ZJvT2oSynqyuJS5MbSwqHl2nyeao8Z8BNVe3 -EOKQAWHUUzpf1xy0/p6Eb4LQObxl9N8sJ3zHX/CcwMpcgh/NRsbQn+TiKJwVkGa3 -SU+UxKlCD1+rTkIr//J91njgictfoXnbAgMBAAGjgbUwgbIwDAYDVR0TAQH/BAIw -ADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC -MDMGA1UdEQQsMCqCF3ZhbGlkLWxlYWYuZXhhbXBsZS50ZXN0gglsb2NhbGhvc3SH -BH8AAAEwHwYDVR0jBBgwFoAUggm6S2XB2Q3i/qyBFlKqXtfiKWIwHQYDVR0OBBYE -FEqSu+i1sHYHANJN5DzvUCZYo6hhMA0GCSqGSIb3DQEBCwUAA4IBAQCDYdk9rdz9 -/q4WvH3FwCA5BwFuiaglL8w0k0DhRCuJBd94YdnmMqNbf8C4elJJF6z1nH6Fv2+9 -2KFQ26VsTchGSDrTy+GGT9F7xMQl/pXAvH0aQs/Km1egtWV6GKb0z1HPqyni0Xet -swJZ6JKlArKjUKGzKCq7fKBm6BR9xvPj5K2wsfRAozUUFfaOou/jBZ0PDpAZoyZb -MDMh0EfcvKsOh2VesMsfK3bwXbCScjmkSsIebTC+jWHKpoEm2SlVHAMJRSRWIVV1 -AgKMYcwLQRNcslkS7I6HtEjF79vhR54Yu5kxSuU9QiGgYmccfUrzxikg5M+s7nQm -zbQhWJ68ME07 ------END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key deleted file mode 100644 index a5070c25188cc..0000000000000 --- a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/valid_key_with_expired_ca.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAnrMQR01axk522e2n77BxBNcaVQ3QPHmYDmpBWuiSVuM8HURi -H1G8P8lkwYmaatiNV4u3W2OkXh9PVLW1lDwl64We9VauDDthUndZJ2alyKKo1/Y0 -QBk+SfllN7TzS9mje3xkkERykfZjv02IU3/ckBgkR886SzRydUVzihJHZobGWd+s -M9Cylj7NNYMxbE56fE+EqbbSa8ltn++KtGSb09qEsp6sriUuTG0sKh5dp8nmqPGf -ATVXtxDikAFh1FM6X9cctP6ehG+C0Dm8ZfTfLCd8x1/wnMDKXIIfzUbG0J/k4iic -FZBmt0lPlMSpQg9fq05CK//yfdZ44InLX6F52wIDAQABAoIBACuB7dGObHd3ZXAD -jonQtntcOVTeD5u0vjIdgUNaBVyauY1QhRIPB2v5W40Pm2z1Z3J71E3SuGoxbT9M -/bXg84hpPpYGKHskAF9qZt+9bW/e4Cksz1BPW1tOayhljFncFcyx4qQGj95iTSBS -MjCqGh7K749cSZ/6hfKOkslkj3yUwdlA8I27MO4iDMhPj6z+eubHV2G3ypfefGlD -QQ7ljuFodobtR7udUVWYnPhuScJyUvHG//uCMaLLaW2rWXD3jv0MUbIfTdYUetig -p0swMeZGUmGh1zlvlJ+l2LBJMXeEi6gzWsHaRO6JJHw/nq64nVb8mq7dCiljRVGv -G8THEFkCgYEAzoS7zVl1A4M/DbJLJ7i/ZWlT48McAizYx9oq1eAgObrdmDe2Jbvm -TIwgS1RUfjfViRwgWbuR0X7xt9OIT2QnDGqkOnDJzu764XtLT/D4TNJA1QG3H4L2 -Jy0JksMLhoUXIormGhAV0onSSfNV5SqHt//OrDAQKwfCIUXh9+62LbcCgYEAxLk+ -sEtHp9EzUGYayUbXghWk1u3/RiuEtlx2sS3v+qtbOGAfyXzPYAHJVFiHFo2/JG1Q -rsYVx5rj/YoQwiFHjR0lkLeEpRqhXv1s1goaUiuBVxzmHX8V97QPLfzEuQLabZwM -9pWLH8zGTKrSIooRb4I1HU7ooElVYDX02JYcFP0CgYEAiJc953ntbN9XyuVL0//b -h2V8uL4JPl8PGk/v2PmeFtDDU7Q1Ywu+LI7ZpTknkTu4njDeLLtknJ1LnnvoQipJ -sWqvKIAE2jsx8ASuMTd94sGFY9z4k3z49bxSAqHCc7x/KreXrVFKPbAuR/8LpsDU -dxxYQ4aeivdcrMkdxfA6yk0CgYAEhgn1/dUo+7uFVsO46yMbf6nps1FSaL/FfbzQ -+DBzgCs50aQJexA9sezSPrLkht/lU4ouaqmnjF0/wEQAYsmFai0p9b5cGY+qYoN1 -LIhMaWmw+h4kgX6c0owiz5QqePFS4eq+ZNPtKEVLEAaC+s/J06GrCdx5ixYmfzch -H9qHdQKBgAvOPI7f8bLNJManzHfcnnNnxrKYg1J3YNvRJa+QkGR3sXQULT2NRiT/ -35NrOnKo0pwCgVMgO2zkSG3jQMCG9Yd9hE5fRHCwjhMUK5i7R4xS5FwrvowO+IMW -g0yyx7tg4keTdlKo9KuJSVlv+tnRIX8ZmYxBMcdtIbLEoMTbA4Yh ------END RSA PRIVATE KEY-----