From 9eea16f3b50ef00ce0fa0c038fe90c0ddb075f01 Mon Sep 17 00:00:00 2001 From: Asif Bashar Date: Wed, 8 Jul 2026 15:16:20 -0700 Subject: [PATCH] read cert from header when trusted proxy sets it Signed-off-by: Asif Bashar --- .../http/CertificateAuthenticationTest.java | 39 ++++++++- .../ssl/OpenSearchSecuritySSLPlugin.java | 9 ++ .../ssl/util/HeaderClientCertResolver.java | 82 +++++++++++++++++++ .../security/ssl/util/SSLConfigConstants.java | 9 ++ .../security/ssl/util/SSLRequestHelper.java | 12 ++- 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/opensearch/security/ssl/util/HeaderClientCertResolver.java diff --git a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java index 581254346f..24e9885ecc 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java @@ -9,13 +9,17 @@ */ package org.opensearch.security.http; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; +import org.apache.hc.core5.http.message.BasicHeader; import org.junit.ClassRule; import org.junit.Test; +import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; @@ -66,7 +70,14 @@ public class CertificateAuthenticationTest { @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().nodeSettings( - Map.of("plugins.security.ssl.http.clientauth_mode", "OPTIONAL") + Map.of( + "plugins.security.ssl.http.clientauth_mode", + "OPTIONAL", + SSLConfigConstants.SECURITY_SSL_HTTP_USE_HEADER_CERT, + "true", + SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_ALLOWED_PROXY_PRINCIPLE, + "DC=de,L=test,O=users,OU=bridge,CN=spock" + ) ) .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .anonymousAuth(false) @@ -164,6 +175,32 @@ public void shouldRetrieveBackendRoleFromCertificate_positiveRoleBridge() { } } + @Test + /** + * The test will result in resolving to user kirk associated role because header cert will get priority. + * In a external load balancer where TLS handshake is done at load balancer, opensearch will only get load balancer certificate + * in handshake. To pass original client cert to opensearch, it can be passed as header. OpenSearch can use this header to resolve + * the role of original client instead of using load balancer certificate. This is specially useful in kubernetes environment + * where load balancer is the only way to connect to the cluster in many cases. + */ + public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain_fromheader() { + CertificateData userKirkCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK); + CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_SPOCK); + + String encodedKirkCert = URLEncoder.encode(userKirkCertificate.certificateInPemFormat(), StandardCharsets.UTF_8); + try (TestRestClient client = cluster.getRestClient(userSpockCertificate, new BasicHeader("x-client-cert", encodedKirkCert))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(0)); + } + } + @Test public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain() { CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK); diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index 5a4780f875..163aad5c1c 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -569,6 +569,15 @@ public List> getSettings() { settings.add( Setting.simpleString(SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, Property.NodeScope, Property.Filtered) ); + settings.add(Setting.simpleString(SSLConfigConstants.SECURITY_SSL_HTTP_USE_HEADER_CERT, Property.NodeScope, Property.Filtered)); + settings.add(Setting.simpleString(SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_NAME, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.simpleString( + SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_ALLOWED_PROXY_PRINCIPLE, + Property.NodeScope, + Property.Filtered + ) + ); settings.add(Setting.simpleString(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_FILE, Property.NodeScope, Property.Filtered)); settings.add(Setting.boolSetting(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATE, false, Property.NodeScope, Property.Filtered)); diff --git a/src/main/java/org/opensearch/security/ssl/util/HeaderClientCertResolver.java b/src/main/java/org/opensearch/security/ssl/util/HeaderClientCertResolver.java new file mode 100644 index 0000000000..5cc8e171ec --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/util/HeaderClientCertResolver.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.ssl.util; + +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.support.WildcardMatcher; + +public class HeaderClientCertResolver { + private static final Logger log = LogManager.getLogger(HeaderClientCertResolver.class); + + public static SSLRequestHelper.SSLInfo maybeOverride( + Settings settings, + SecurityRequest request, + X509Certificate[] x509Certs, + String principal, + PrincipalExtractor principalExtractor, + String protocol, + String cipher, + X509Certificate[] localCerts + ) { + final String clientCertHeaderName = settings.get(SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_NAME, "x-client-cert"); + // SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_ALLOWED_PROXY_PRINCIPLE configuration used to specify which principle can switch + // user to header cert so its limited to external load balancer proxy , not all clients + final WildcardMatcher allowedPrinciplesToAssumeHeaderCert = WildcardMatcher.from( + settings.getAsList(SSLConfigConstants.SECURITY_SSL_HTTP_HEADER_CERT_ALLOWED_PROXY_PRINCIPLE) + ); + + final boolean allowRoleFromHeaderCert = Boolean.parseBoolean( + settings.get(SSLConfigConstants.SECURITY_SSL_HTTP_USE_HEADER_CERT, "false") + ); + + String clientCert = request.header(clientCertHeaderName); + // we want to make x509Certs is not null and has already been validated through handshake , + // only then we allow header based cert role assumption + if (clientCert != null && allowRoleFromHeaderCert && x509Certs != null) { + if (allowedPrinciplesToAssumeHeaderCert.test(principal)) { + log.trace("Client Cert Encoded : {} ", clientCert); + clientCert = URLDecoder.decode(clientCert, StandardCharsets.UTF_8); + log.trace("Client Cert From Header : {} ", clientCert); + + byte[] decodedClientCert = clientCert.getBytes(StandardCharsets.UTF_8); + + CertificateFactory factory = null; + try { + factory = CertificateFactory.getInstance("X.509"); + X509Certificate[] x509HeaderCerts = new X509Certificate[1]; + x509HeaderCerts[0] = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decodedClientCert)); + principal = principalExtractor == null + ? null + : principalExtractor.extractPrincipal(x509HeaderCerts[0], PrincipalExtractor.Type.HTTP); + // At this point if no exception in parsing, then replace original certs that were used in handshake validation to + // header + // cert + x509Certs = x509HeaderCerts; + } catch (CertificateException e) { + log.error("Failed to parse certificate from header", e); + } + } + } + return new SSLRequestHelper.SSLInfo(x509Certs, principal, protocol, cipher, localCerts); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index 689c41238e..6754027a65 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -100,6 +100,15 @@ public final class SSLConfigConstants { + ENFORCE_CERT_RELOAD_DN_VERIFICATION; public static final String SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH = SSL_HTTP_PREFIX + PEM_TRUSTED_CAS_FILEPATH; + // header cert resolve + public static final String USE_HEADER_CERT = "clientauth_use_header_cert"; + public static final String HEADER_CERT_NAME = "clientauth_header_cert_name"; + public static final String ALLOWED_PROXY_PRINCIPLE_TO_USE_HEADER_CERT = "clientauth_header_cert_allowed_for_principle"; + public static final String SECURITY_SSL_HTTP_USE_HEADER_CERT = SSL_HTTP_PREFIX + USE_HEADER_CERT; + public static final String SECURITY_SSL_HTTP_HEADER_CERT_NAME = SSL_HTTP_PREFIX + HEADER_CERT_NAME; + public static final String SECURITY_SSL_HTTP_HEADER_CERT_ALLOWED_PROXY_PRINCIPLE = SSL_HTTP_PREFIX + + ALLOWED_PROXY_PRINCIPLE_TO_USE_HEADER_CERT; + // http cert revocation list settings public static final String SECURITY_SSL_HTTP_CRL_FILE = SSL_HTTP_CRL_PREFIX + "file_path"; public static final String SECURITY_SSL_HTTP_CRL_VALIDATE = SSL_HTTP_CRL_PREFIX + "validate"; diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java index 2dd4bef9c4..c0652cf91e 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java @@ -156,8 +156,16 @@ public static SSLInfo getSSLInfo( if (session.getLocalCertificates() != null) { localCerts = Arrays.stream(session.getLocalCertificates()).map(X509Certificate.class::cast).toArray(X509Certificate[]::new); } - - return new SSLInfo(x509Certs, principal, protocol, cipher, localCerts); + return HeaderClientCertResolver.maybeOverride( + settings, + request, + x509Certs, + principal, + principalExtractor, + protocol, + cipher, + localCerts + ); } private static void validatePeerCerts(final X509Certificate[] x509Certs, final Settings settings, final Path configPath)