Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<String> backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(backendRoles, hasSize(1));
assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN));
List<String> roles = response.getTextArrayFromJsonBody(POINTER_ROLES);
assertThat(roles, hasSize(0));
}
}

@Test
public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain() {
CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,15 @@ public List<Setting<?>> 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading