diff --git a/.github/workflows/build/conf/topologies/knoxldap.xml b/.github/workflows/build/conf/topologies/knoxldap.xml
index 884e8bc5e6..3b886b777c 100644
--- a/.github/workflows/build/conf/topologies/knoxldap.xml
+++ b/.github/workflows/build/conf/topologies/knoxldap.xml
@@ -35,7 +35,7 @@ limitations under the License.
main.ldapRealm.contextFactory.url
- ldap://localhost:33390
+ ldaps://localhost:33390
main.ldapRealm.contextFactory.authenticationMechanism
diff --git a/.github/workflows/build/gateway-site.xml b/.github/workflows/build/gateway-site.xml
index 8f35a939f8..36e884a3ef 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -153,6 +153,20 @@ limitations under the License.
gateway.ldap.interceptor.names
demoldap
+
+
+ gateway.ldap.ssl.enabled
+ true
+
+
+ gateway.ldap.ssl.keystore.path
+ /knox-runtime/conf/ldaps-keystore.p12
+
+
+ gateway.ldap.ssl.keystore.password.alias
+ gateway_ldap_ssl_keystore_password
+
@@ -165,7 +179,13 @@ limitations under the License.
gateway.ldap.interceptor.demoldap.url
- ldap://ldap:33389
+ ldaps://ldap:33389
+
+
+
+ gateway.ldap.interceptor.demoldap.trustAllCertificates
+ true
gateway.ldap.interceptor.demoldap.remoteBaseDn
diff --git a/.github/workflows/build/gateway.sh b/.github/workflows/build/gateway.sh
index f31dd441c4..e4927b7b28 100755
--- a/.github/workflows/build/gateway.sh
+++ b/.github/workflows/build/gateway.sh
@@ -13,7 +13,37 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+set -e
+# The embedded Knox LDAP service is exposed over LDAPS (see gateway-site.xml:
+# gateway.ldap.ssl.enabled). Provision a dev-only certificate for it and make the
+# JVM trust that certificate so the knoxldap Shiro realm - which authenticates over
+# ldaps://localhost:33390 via JNDI - can validate it.
+KEYSTORE=/knox-runtime/conf/ldaps-keystore.p12
+KEYSTORE_PASSWORD=knox-ldap-ssl-password
+KEYSTORE_PASSWORD_ALIAS=gateway_ldap_ssl_keystore_password
-# Start Knox
-java -jar /knox-runtime/bin/gateway.jar
\ No newline at end of file
+# 1) Self-signed dev certificate for the embedded LDAP service's secure transport.
+# Store and key passwords must match (ApacheDS opens the store and recovers the
+# key with a single password). PKCS12 matches the JVM default keystore type.
+keytool -genkeypair -alias ldaps -keyalg RSA -keysize 2048 \
+ -dname "CN=localhost" -ext "san=dns:localhost,dns:knox" -validity 3650 \
+ -keystore "$KEYSTORE" -storetype PKCS12 \
+ -storepass "$KEYSTORE_PASSWORD" -keypass "$KEYSTORE_PASSWORD"
+
+# 2) Store the keystore password under the alias the LDAP SSL config resolves.
+/knox-runtime/bin/knoxcli.sh create-alias "$KEYSTORE_PASSWORD_ALIAS" --value "$KEYSTORE_PASSWORD"
+
+# 3) Trust that certificate in the JVM default truststore (cacerts) so the JNDI-based
+# Shiro LDAP realm accepts it. This is additive - it does not remove the default CAs.
+keytool -exportcert -alias ldaps -rfc \
+ -keystore "$KEYSTORE" -storepass "$KEYSTORE_PASSWORD" -file /tmp/ldaps-cert.pem
+keytool -importcert -noprompt -alias knox-ldaps \
+ -keystore "${JAVA_HOME}/lib/security/cacerts" -storepass changeit \
+ -file /tmp/ldaps-cert.pem
+rm -f /tmp/ldaps-cert.pem
+
+# Start Knox. Endpoint (hostname) identification is disabled for the embedded LDAPS
+# connection - the dev cert is self-signed - but trust is still enforced via cacerts.
+java -Dcom.sun.jndi.ldap.object.disableEndpointIdentification=true \
+ -jar /knox-runtime/bin/gateway.jar
diff --git a/.github/workflows/build/ldap.sh b/.github/workflows/build/ldap.sh
index ad3d9c8e3f..fda7eaa928 100755
--- a/.github/workflows/build/ldap.sh
+++ b/.github/workflows/build/ldap.sh
@@ -13,4 +13,22 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-java -jar /knox-runtime/bin/ldap.jar /knox-runtime/conf
\ No newline at end of file
+set -e
+
+# Expose the demo LDAP over a secure (LDAPS) transport so the Knox backend can be
+# configured to proxy to it over TLS. Generate a self-signed, dev-only certificate on
+# each start (the demo LDAP is a throwaway fixture, not a secret). The store and key
+# passwords must match: ApacheDS opens the keystore and recovers the key with a single
+# password. PKCS12 matches the JVM default keystore type ApacheDS loads it with.
+KEYSTORE=/knox-runtime/conf/ldap-keystore.p12
+KEYSTORE_PASSWORD=ldap-keystore-password
+
+keytool -genkeypair -alias ldaps -keyalg RSA -keysize 2048 \
+ -dname "CN=ldap" -ext "san=dns:ldap,dns:localhost" -validity 3650 \
+ -keystore "$KEYSTORE" -storetype PKCS12 \
+ -storepass "$KEYSTORE_PASSWORD" -keypass "$KEYSTORE_PASSWORD"
+
+java -Dldap.ssl.enabled=true \
+ -Dldap.keystore.file="$KEYSTORE" \
+ -Dldap.keystore.password="$KEYSTORE_PASSWORD" \
+ -jar /knox-runtime/bin/ldap.jar /knox-runtime/conf
\ No newline at end of file
diff --git a/.github/workflows/tests/test_knox_ldap_proxy_search.py b/.github/workflows/tests/test_knox_ldap_proxy_search.py
index 9d015551ed..bb42336ade 100644
--- a/.github/workflows/tests/test_knox_ldap_proxy_search.py
+++ b/.github/workflows/tests/test_knox_ldap_proxy_search.py
@@ -18,17 +18,25 @@
These exercise the LdapProxyBackend.search() path (KNOX-3341): clients can query
the embedded Knox LDAP service - which proxies to the demo LDAP backend - by
objectClass, cn and uid (including wildcards), not just by a single uid lookup.
+
+The connection is made over LDAPS: the embedded Knox LDAP service is configured
+with gateway.ldap.ssl.enabled=true, and it in turn proxies to the demo LDAP
+backend over LDAPS as well (gateway.ldap.interceptor.demoldap.url=ldaps://...).
+The gateway presents a self-signed dev certificate in CI, so certificate
+validation is disabled on the client side.
"""
from __future__ import annotations
import os
+import ssl
import unittest
from urllib.parse import urlparse
import ldap3
-# The embedded Knox LDAP service port (see gateway-site.xml: gateway.ldap.port).
+# The embedded Knox LDAP service secure port (see gateway-site.xml: gateway.ldap.port
+# with gateway.ldap.ssl.enabled=true).
KNOX_LDAP_PORT = 33390
BASE_DN = "dc=hadoop,dc=apache,dc=org"
@@ -50,7 +58,11 @@ class TestKnoxLdapProxySearch(unittest.TestCase):
"""Verify general search requests are proxied to the demo LDAP backend."""
def setUp(self) -> None:
- server = ldap3.Server(knox_host(), port=KNOX_LDAP_PORT, get_info=ldap3.NONE)
+ # Self-signed dev certificate in CI: connect over TLS but skip validation.
+ tls = ldap3.Tls(validate=ssl.CERT_NONE)
+ server = ldap3.Server(
+ knox_host(), port=KNOX_LDAP_PORT, use_ssl=True, tls=tls, get_info=ldap3.NONE
+ )
self.connection = ldap3.Connection(
server, user=BIND_DN, password=BIND_PASSWORD, auto_bind=True
)
diff --git a/gateway-demo-ldap/pom.xml b/gateway-demo-ldap/pom.xml
index 96afc99803..55eea594f5 100644
--- a/gateway-demo-ldap/pom.xml
+++ b/gateway-demo-ldap/pom.xml
@@ -90,6 +90,13 @@
org.bouncycastle
bcprov-jdk18on
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
diff --git a/gateway-demo-ldap/src/main/java/org/apache/knox/gateway/security/ldap/SimpleLdapDirectoryServer.java b/gateway-demo-ldap/src/main/java/org/apache/knox/gateway/security/ldap/SimpleLdapDirectoryServer.java
index 1561e325e5..f3e5624196 100644
--- a/gateway-demo-ldap/src/main/java/org/apache/knox/gateway/security/ldap/SimpleLdapDirectoryServer.java
+++ b/gateway-demo-ldap/src/main/java/org/apache/knox/gateway/security/ldap/SimpleLdapDirectoryServer.java
@@ -104,6 +104,24 @@ public SimpleLdapDirectoryServer( String rootDn, File usersLdif, Transport... tr
server = new LdapServer();
server.setTransports( transports );
server.setDirectoryService( service );
+
+ // Optionally expose a secure (LDAPS) transport. Controlled by system properties so the
+ // demo server can be launched in either plaintext or secure mode without code changes.
+ if ( Boolean.parseBoolean( System.getProperty( "ldap.ssl.enabled", "false" ) ) ) {
+ for ( Transport transport : transports ) {
+ if ( transport instanceof TcpTransport ) {
+ ( (TcpTransport) transport ).setEnableSSL( true );
+ }
+ }
+ String keystoreFile = System.getProperty( "ldap.keystore.file" );
+ if ( keystoreFile != null && !keystoreFile.isEmpty() ) {
+ server.setKeystoreFile( keystoreFile );
+ }
+ String keystorePassword = System.getProperty( "ldap.keystore.password" );
+ if ( keystorePassword != null ) {
+ server.setCertificatePassword( keystorePassword );
+ }
+ }
}
private static void enabledPosixSchema( DirectoryService service ) throws LdapException {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index d401f30802..061518537d 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -1863,6 +1863,27 @@ public String getLdapRolesLookupFilePath() {
return get(LDAP_ROLES_LOOKUP_FILE_PATH);
}
+ @Override
+ public boolean isLDAPSSLEnabled() {
+ return Boolean.parseBoolean(get(LDAP_SSL_ENABLED, "false"));
+ }
+
+ @Override
+ public String getLDAPSSLKeystorePath() {
+ return get(LDAP_SSL_KEYSTORE_PATH, null);
+ }
+
+ @Override
+ public String getLDAPSSLKeystorePasswordAlias() {
+ return get(LDAP_SSL_KEYSTORE_PASSWORD_ALIAS, null);
+ }
+
+ @Override
+ public List getLDAPSSLEnabledCipherSuites() {
+ final List cipherSuites = splitConfigValueToList(LDAP_SSL_ENABLED_CIPHER_SUITES);
+ return cipherSuites == null ? Collections.emptyList() : cipherSuites;
+ }
+
@Override
public boolean getGroupUIServicesOnHomepage() {
return getBoolean(KNOX_HOMEPAGE_GROUP_UI_SERVICES, DEFAULT_GROUP_UI_SERVICES);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
index 5d41d77105..4f30fa80ca 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
@@ -79,6 +79,11 @@ public class KnoxLDAPServerManager {
private int port;
private String baseDn;
private String bindUser;
+ // Secure (LDAPS) transport configuration
+ private boolean sslEnabled;
+ private String sslKeystorePath;
+ private String sslKeystorePasswordAlias;
+ private List sslEnabledCipherSuites;
// Collection of DNs for the proxied backend LDAP servers
private Set baseDns;
@@ -108,6 +113,17 @@ public void initialize(GatewayConfig config) throws Exception {
this.baseDn = config.getLDAPBaseDN();
this.bindUser = config.getLDAPBindUser();
+ // Secure (LDAPS) transport configuration. When enabled but no dedicated keystore is
+ // configured, fall back to the gateway identity keystore so the embedded server can
+ // reuse the gateway's own TLS material out of the box.
+ this.sslEnabled = config.isLDAPSSLEnabled();
+ if (sslEnabled) {
+ final String configuredKeystore = config.getLDAPSSLKeystorePath();
+ this.sslKeystorePath = StringUtils.isNotBlank(configuredKeystore) ? configuredKeystore : config.getIdentityKeystorePath();
+ this.sslKeystorePasswordAlias = config.getLDAPSSLKeystorePasswordAlias();
+ this.sslEnabledCipherSuites = config.getLDAPSSLEnabledCipherSuites();
+ }
+
createInterceptors(config);
// Clean up previous run if it didn't shut down cleanly
@@ -215,7 +231,11 @@ public void start() throws Exception {
// Create LDAP server on configured port
ldapServer = new LdapServer();
- ldapServer.setTransports(new TcpTransport(port));
+ final TcpTransport transport = new TcpTransport(port);
+ if (sslEnabled) {
+ configureSsl(transport);
+ }
+ ldapServer.setTransports(transport);
ldapServer.setDirectoryService(directoryService);
ldapServer.start();
@@ -280,6 +300,56 @@ private int fetchAuthenticationInterceptorIndex(final List dsInterc
.orElse(-1);
}
+ /**
+ * Enable the secure (LDAPS) transport on the embedded server. The keystore holding the
+ * server certificate and its password are resolved from the gateway configuration and
+ * credential store; the password defaults to the gateway identity keystore password when
+ * no dedicated alias is configured. The keystore must be in the JVM default format
+ * (see {@code java.security.KeyStore#getDefaultType()}), which is what ApacheDS uses to
+ * load it.
+ */
+ private void configureSsl(final TcpTransport transport) throws Exception {
+ if (StringUtils.isBlank(sslKeystorePath)) {
+ final String reason = "no keystore configured; set " + GatewayConfig.LDAP_SSL_KEYSTORE_PATH
+ + " or configure the gateway identity keystore";
+ LOG.ldapSslConfigInvalid(reason);
+ throw new IllegalStateException("Cannot enable LDAPS: " + reason);
+ }
+
+ final File keystore = new File(sslKeystorePath);
+ if (!keystore.exists()) {
+ final String reason = "keystore file not found: " + keystore.getAbsolutePath();
+ LOG.ldapSslConfigInvalid(reason);
+ throw new IllegalStateException("Cannot enable LDAPS: " + reason);
+ }
+
+ transport.setEnableSSL(true);
+ ldapServer.setKeystoreFile(keystore.getAbsolutePath());
+
+ final char[] passwordChars = resolveSslKeystorePassword();
+ if (passwordChars != null) {
+ ldapServer.setCertificatePassword(new String(passwordChars));
+ }
+
+ if (sslEnabledCipherSuites != null && !sslEnabledCipherSuites.isEmpty()) {
+ ldapServer.setEnabledCipherSuites(new ArrayList<>(sslEnabledCipherSuites));
+ }
+
+ LOG.ldapSslEnabled(keystore.getAbsolutePath());
+ }
+
+ /**
+ * Resolve the password protecting the LDAPS keystore from the gateway credential store.
+ * Uses the configured alias when set, otherwise falls back to the gateway identity
+ * keystore password (matching the keystore-path fallback in {@link #initialize(GatewayConfig)}).
+ */
+ private char[] resolveSslKeystorePassword() throws Exception {
+ if (StringUtils.isNotBlank(sslKeystorePasswordAlias)) {
+ return aliasService.getPasswordFromAliasForGateway(sslKeystorePasswordAlias);
+ }
+ return aliasService.getGatewayIdentityKeystorePassword();
+ }
+
/**
* Stop the LDAP server
*/
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
index a69ad7cd86..816fef0035 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
@@ -33,6 +33,14 @@ public interface LdapMessages {
text = "LDAP service started successfully on port {0}")
void ldapServiceStarted(int port);
+ @Message(level = MessageLevel.INFO,
+ text = "Enabling secure (LDAPS) transport for LDAP service using keystore: {0}")
+ void ldapSslEnabled(String keystorePath);
+
+ @Message(level = MessageLevel.ERROR,
+ text = "Cannot enable secure (LDAPS) transport: {0}")
+ void ldapSslConfigInvalid(String reason);
+
@Message(level = MessageLevel.INFO,
text = "Anonymous access disabled; clients must bind as: {0}")
void ldapBindUserConfigured(String bindDn);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
index 300acff393..8466f5b12b 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
@@ -36,8 +36,18 @@
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ldap.LdapMessages;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -87,6 +97,18 @@ public class LdapProxyBackend implements LdapBackend {
private final String proxyEntryGroupMembershipAttributeType = "memberOf";
+ // Secure transport configuration for the connection to the remote LDAP server
+ private boolean useSsl; // LDAPS (implicit TLS)
+ private boolean trustAllCertificates; // skip certificate validation (test/dev only)
+ private String trustStorePath;
+ private String trustStoreType;
+ private String trustStorePassword;
+ private String sslProtocol;
+ private String[] enabledProtocols;
+ private String[] enabledCipherSuites;
+ // LDAP response timeout in milliseconds; negative means use the client default.
+ private long connectionTimeoutMillis = -1;
+
// Connection pool for efficient connection reuse
private LdapConnectionPool connectionPool;
@@ -160,13 +182,31 @@ public LdapProxyBackend(String name, Map config) {
recursiveGroupResolution = Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false"));
recursiveGroupResolutionMaxDepth = Integer.parseInt(config.getOrDefault("recursiveGroupResolutionMaxDepth", "3"));
+ // Configure secure transport (LDAPS) to the remote server. An ldaps:// URL enables it
+ // by default; an explicit useSsl setting always wins.
+ final boolean ldapsFromUrl = ldapUrl != null && ldapUrl.toLowerCase(Locale.ROOT).startsWith("ldaps://");
+ useSsl = Boolean.parseBoolean(config.getOrDefault("useSsl", String.valueOf(ldapsFromUrl)));
+ trustAllCertificates = Boolean.parseBoolean(config.getOrDefault("trustAllCertificates", "false"));
+ trustStorePath = config.get("trustStore");
+ trustStoreType = config.getOrDefault("trustStoreType", KeyStore.getDefaultType());
+ trustStorePassword = config.get("trustStorePassword");
+ sslProtocol = config.get("sslProtocol");
+ enabledProtocols = splitToArray(config.get("enabledProtocols"));
+ enabledCipherSuites = splitToArray(config.get("enabledCipherSuites"));
+ final String timeout = config.get("connectionTimeout");
+ if (timeout != null && !timeout.trim().isEmpty()) {
+ connectionTimeoutMillis = Long.parseLong(timeout.trim());
+ }
+
// Build search filter template
userSearchFilter = "(" + remoteUserIdentifierAttribute + "={username})";
LOG.ldapBackendLoading(getName(), "Proxying " + proxyBaseDn + " to " + ldapUrl + " (" + remoteBaseDn + ") with " +
remoteUserIdentifierAttribute + " attribute" +
(useMemberOf ? " using memberOf lookups" : " using group searches") +
- (recursiveGroupResolution ? " with recursive group resolution (max depth: " + recursiveGroupResolutionMaxDepth + ")" : ""));
+ (recursiveGroupResolution ? " with recursive group resolution (max depth: " + recursiveGroupResolutionMaxDepth + ")" : "") +
+ (useSsl ? " over LDAPS" : "") +
+ ((useSsl && trustAllCertificates) ? " (certificate validation disabled)" : ""));
// Initialize connection pool
initializeConnectionPool(config);
@@ -198,6 +238,8 @@ private void initializeConnectionPool(Map config) {
LdapConnectionConfig connectionConfig = new LdapConnectionConfig();
connectionConfig.setLdapHost(host);
connectionConfig.setLdapPort(port);
+ applySslConfiguration(connectionConfig);
+ applyTimeout(connectionConfig);
if (bindDn != null && !bindDn.isEmpty()) {
connectionConfig.setName(bindDn);
@@ -254,6 +296,91 @@ private void parseLdapUrl(String url) {
}
}
+ /**
+ * Apply the configured LDAPS settings to a connection configuration. This is shared by the
+ * pooled search connections and the short-lived authentication (bind) connection so both
+ * talk to the remote server over the same secure transport.
+ */
+ private void applySslConfiguration(LdapConnectionConfig connectionConfig) {
+ if (!useSsl) {
+ return;
+ }
+ connectionConfig.setUseSsl(true);
+ connectionConfig.setTrustManagers(buildTrustManagers());
+ if (sslProtocol != null && !sslProtocol.isEmpty()) {
+ connectionConfig.setSslProtocol(sslProtocol);
+ }
+ if (enabledProtocols != null) {
+ connectionConfig.setEnabledProtocols(enabledProtocols);
+ }
+ if (enabledCipherSuites != null) {
+ connectionConfig.setEnabledCipherSuites(enabledCipherSuites);
+ }
+ }
+
+ /** Apply the configured LDAP response timeout, when set, to a connection configuration. */
+ private void applyTimeout(LdapConnectionConfig connectionConfig) {
+ if (connectionTimeoutMillis >= 0) {
+ connectionConfig.setTimeout(connectionTimeoutMillis);
+ }
+ }
+
+ /**
+ * Build the trust managers used to validate the remote server's certificate:
+ *
+ * - {@code trustAllCertificates=true} disables validation (test/dev only);
+ * - a configured {@code trustStore} is loaded and used as the trust anchor;
+ * - otherwise the JVM default trust store is used.
+ *
+ */
+ private TrustManager[] buildTrustManagers() {
+ try {
+ if (trustAllCertificates) {
+ return new TrustManager[] { trustAllTrustManager() };
+ }
+ KeyStore trustStore = null;
+ if (trustStorePath != null && !trustStorePath.isEmpty()) {
+ trustStore = KeyStore.getInstance(trustStoreType);
+ final char[] password = trustStorePassword == null ? null : trustStorePassword.toCharArray();
+ try (InputStream is = Files.newInputStream(Paths.get(trustStorePath))) {
+ trustStore.load(is, password);
+ }
+ }
+ // A null KeyStore makes the factory fall back to the JVM default trust store.
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+ return tmf.getTrustManagers();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to configure trust material for LDAP backend " + getName(), e);
+ }
+ }
+
+ /** A trust manager that accepts any certificate; only used when trustAllCertificates is set. */
+ private static X509TrustManager trustAllTrustManager() {
+ return new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) { }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) { }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+ }
+
+ private static String[] splitToArray(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ return null;
+ }
+ return Arrays.stream(value.split(","))
+ .map(String::trim)
+ .filter(part -> !part.isEmpty())
+ .toArray(String[]::new);
+ }
+
/**
* Gets a connection from the connection pool.
* Connections obtained from this method should be released back to the pool
@@ -307,6 +434,8 @@ public boolean authenticate(Dn userDn, String password) {
final LdapConnectionConfig authConfig = new LdapConnectionConfig();
authConfig.setLdapHost(host);
authConfig.setLdapPort(port);
+ applySslConfiguration(authConfig);
+ applyTimeout(authConfig);
authConfig.setName(remoteUserDnText);
authConfig.setCredentials(password);
try (LdapConnection connection = new LdapNetworkConnection(authConfig)){
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
index b277cb8351..78631552c0 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
@@ -23,12 +23,14 @@
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapProtocolErrorException;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.util.X509CertificateUtil;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory;
@@ -42,8 +44,21 @@
import org.junit.Before;
import org.junit.After;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
import java.io.File;
+import java.io.OutputStream;
import java.net.ServerSocket;
+import java.nio.file.Files;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -60,6 +75,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
/**
* Unit tests for KnoxLDAPServerManager.
@@ -68,10 +84,13 @@ public class KnoxLDAPServerManagerTest {
private static final String BIND_DN = "uid=knox,ou=system";
private static final String BIND_PASSWORD = "knox-password";
+ private static final String KEYSTORE_PASSWORD = "keystore-password";
+ private static final String SSL_KEYSTORE_ALIAS = "gateway_ldap_ssl_keystore_password";
private KnoxLDAPServerManager serverManager;
private File tempWorkDir;
private File tempLdapFile;
+ private File tempKeystore;
private int port;
@Before
@@ -517,6 +536,116 @@ public void testAnonymousStillAllowedWhenUnconfigured() throws Exception {
}
}
+ @Test
+ public void testLdapsTransportPresentsConfiguredCertificate() throws Exception {
+ useKeystorePassword(KEYSTORE_PASSWORD);
+ serverManager.initialize(createSslEnabledConfig(createTempKeystore()));
+ serverManager.start();
+
+ assertTrue("Server should be running", serverManager.isRunning());
+
+ // A TLS handshake must succeed on the configured port and present the certificate
+ // loaded from our keystore, proving the transport is genuinely secured (LDAPS).
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[] { trustAllManager() }, new SecureRandom());
+ try (SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket("localhost", port)) {
+ socket.setSoTimeout(5000);
+ socket.startHandshake();
+ Certificate[] serverCerts = socket.getSession().getPeerCertificates();
+ assertTrue("Server should present a certificate", serverCerts.length > 0);
+ X509Certificate serverCert = (X509Certificate) serverCerts[0];
+ assertTrue("Server certificate should be the one from the configured keystore",
+ serverCert.getSubjectX500Principal().getName().contains("CN=localhost"));
+ }
+ }
+
+ @Test(expected = LdapProtocolErrorException.class)
+ public void testLdapsTransportRejectsPlaintextConnection() throws Exception {
+ useKeystorePassword(KEYSTORE_PASSWORD);
+ serverManager.initialize(createSslEnabledConfig(createTempKeystore()));
+ serverManager.start();
+
+ // A plaintext client talking to an SSL-only transport must not be able to bind/search.
+ try (LdapNetworkConnection connection = new LdapNetworkConnection("localhost", port)) {
+ connection.setTimeOut(5000);
+ connection.bind();
+ try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
+ cursor.next();
+ }
+ fail("Plaintext access against an LDAPS-only transport should fail");
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testLdapsFailsWhenKeystoreMissing() throws Exception {
+ useKeystorePassword(KEYSTORE_PASSWORD);
+ File missing = new File(tempWorkDir, "does-not-exist.jks");
+ serverManager.initialize(createSslEnabledConfig(missing));
+ serverManager.start();
+ }
+
+ private GatewayConfig createSslEnabledConfig(File keystore) {
+ GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+ expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
+ expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+ expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
+ expect(mockConfig.isLDAPSSLEnabled()).andReturn(true).anyTimes();
+ expect(mockConfig.getLDAPSSLKeystorePath()).andReturn(keystore.getAbsolutePath()).anyTimes();
+ expect(mockConfig.getLDAPSSLKeystorePasswordAlias()).andReturn(SSL_KEYSTORE_ALIAS).anyTimes();
+ expect(mockConfig.getLDAPSSLEnabledCipherSuites()).andReturn(Collections.emptyList()).anyTimes();
+ replay(mockConfig);
+ return mockConfig;
+ }
+
+ /** A trust manager that accepts any certificate; the test cert is self-signed. */
+ private X509TrustManager trustAllManager() {
+ return new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) { }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) { }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+ }
+
+ /**
+ * Build a keystore holding a self-signed certificate and its private key, in the JVM
+ * default keystore format (which is what ApacheDS uses to load the server certificate).
+ */
+ private File createTempKeystore() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ X509Certificate cert = X509CertificateUtil.generateCertificate(
+ "CN=localhost, OU=Test, O=Knox, L=Test, ST=Test, C=US", keyPair, 365, "SHA256withRSA");
+ assertNotNull("Failed to generate a self-signed test certificate", cert);
+
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+ keyStore.setKeyEntry("knox-ldaps", keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(),
+ new Certificate[] { cert });
+
+ tempKeystore = File.createTempFile("knox-ldaps-test", ".ks");
+ tempKeystore.deleteOnExit();
+ try (OutputStream os = Files.newOutputStream(tempKeystore.toPath())) {
+ keyStore.store(os, KEYSTORE_PASSWORD.toCharArray());
+ }
+ return tempKeystore;
+ }
+
+ /** Rebuild the server manager so its credential store resolves any gateway alias to the given keystore password. */
+ private void useKeystorePassword(String password) throws Exception {
+ serverManager = new KnoxLDAPServerManager(aliasServiceReturning(password));
+ }
+
private GatewayConfig createBindEnabledConfig() {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
@@ -569,6 +698,9 @@ private void cleanupTempFiles() {
if (tempLdapFile != null && tempLdapFile.exists()) {
tempLdapFile.delete();
}
+ if (tempKeystore != null && tempKeystore.exists()) {
+ tempKeystore.delete();
+ }
if (tempWorkDir != null && tempWorkDir.exists()) {
// Clean up work directory recursively
deleteRecursively(tempWorkDir);
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
index 88d215c76e..ce681a5617 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
@@ -171,6 +171,7 @@ public void testOnGatewayConfigChanged() throws Exception {
private void setupMockConfig(String backendType) throws Exception {
expect(mockConfig.isLDAPEnabled()).andReturn(true).atLeastOnce();
+ expect(mockConfig.isLDAPSSLEnabled()).andReturn(false).atLeastOnce();
expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).atLeastOnce();
expect(mockConfig.getLDAPRecursiveGroupResolutionMaxDepth()).andReturn(0).atLeastOnce();
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce();
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
new file mode 100644
index 0000000000..96b8e5a8f3
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap.backend;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapTlsHandshakeException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.api.partition.Partition;
+import org.apache.directory.server.core.api.schema.SchemaPartition;
+import org.apache.directory.server.core.factory.JdbmPartitionFactory;
+import org.apache.directory.server.core.factory.PartitionFactory;
+import org.apache.directory.server.core.partition.ldif.LdifPartition;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.knox.gateway.security.ldap.SimpleDirectoryService;
+import org.apache.knox.gateway.services.ldap.SchemaManagerFactory;
+import org.apache.knox.gateway.util.X509CertificateUtil;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Verifies that {@link LdapProxyBackend} can talk to a remote LDAP server over a secure
+ * (LDAPS) transport. An embedded ApacheDS instance is started with SSL enabled and the backend
+ * connects to it over TLS for both searches (pooled connections) and authentication binds.
+ */
+public class LdapProxyBackendSslTest {
+ private static final String KEYSTORE_PASSWORD = "keystore-password";
+
+ private static File keystore;
+ private static TcpTransport transport;
+ private static DirectoryService directoryService;
+ private static LdapServer ldapServer;
+ private static SchemaManager schemaManager;
+ private static int port;
+
+ private LdapProxyBackend ldapProxyBackend;
+
+ @BeforeClass
+ public static void setupBeforeClass() throws Exception {
+ keystore = createTempKeystore();
+
+ directoryService = new SimpleDirectoryService();
+ directoryService.setShutdownHookEnabled(false);
+ schemaManager = SchemaManagerFactory.createSchemaManager();
+ directoryService.setSchemaManager(schemaManager);
+
+ String instanceDirectory = System.getProperty("java.io.tmpdir", "/tmp") + "/server-work-ssl-" + UUID.randomUUID();
+ directoryService.setInstanceLayout(new InstanceLayout(instanceDirectory));
+
+ File workingDirectory = directoryService.getInstanceLayout().getPartitionsDirectory();
+ LdifPartition ldifPartition = new LdifPartition(schemaManager, directoryService.getDnFactory());
+ ldifPartition.setPartitionPath(new File(workingDirectory, "schema").toURI());
+ SchemaPartition schemaPartition = new SchemaPartition(schemaManager);
+ schemaPartition.setWrappedPartition(ldifPartition);
+ directoryService.setSchemaPartition(schemaPartition);
+
+ PartitionFactory partitionFactory = new JdbmPartitionFactory();
+ Partition systemPartition = partitionFactory.createPartition(
+ schemaManager, directoryService.getDnFactory(),
+ "system", "ou=system", 500,
+ new File(directoryService.getInstanceLayout().getPartitionsDirectory(), "system"));
+ partitionFactory.addIndex(systemPartition, "objectClass", 100);
+ directoryService.setSystemPartition(systemPartition);
+ Partition partition = partitionFactory.createPartition(
+ schemaManager, directoryService.getDnFactory(),
+ "people", "dc=hadoop,dc=apache,dc=org", 500,
+ directoryService.getInstanceLayout().getInstanceDirectory());
+ directoryService.addPartition(partition);
+
+ directoryService.startup();
+
+ CoreSession session = directoryService.getAdminSession();
+ File ldifFile = new File(LdapProxyBackendSslTest.class.getResource("/ldap-proxy-backend-test.ldif").toURI());
+ new LdifFileLoader(session, ldifFile, null).execute();
+
+ // Start the embedded server with a secure (LDAPS) transport.
+ transport = new TcpTransport(0);
+ transport.setEnableSSL(true);
+ ldapServer = new LdapServer();
+ ldapServer.setKeystoreFile(keystore.getAbsolutePath());
+ ldapServer.setCertificatePassword(KEYSTORE_PASSWORD);
+ ldapServer.setTransports(transport);
+ ldapServer.setDirectoryService(directoryService);
+ ldapServer.start();
+ port = transport.getAcceptor().getLocalAddress().getPort();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ if (ldapServer != null) {
+ ldapServer.stop();
+ }
+ if (directoryService != null) {
+ directoryService.shutdown();
+ }
+ if (keystore != null) {
+ keystore.delete();
+ }
+ }
+
+ @After
+ public void tearDown() {
+ if (ldapProxyBackend != null) {
+ ldapProxyBackend.close();
+ }
+ }
+
+ @Test
+ public void testGetUserOverLdaps() throws Exception {
+ ldapProxyBackend = new LdapProxyBackend("sslbackend", trustingConfig());
+
+ Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
+ assertNotNull("User should be resolved over LDAPS", entry);
+ assertEquals("ldaptest1", entry.get("uid").getString());
+ validateMemberOf(entry, Set.of(
+ "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
+ "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+ }
+
+ @Test
+ public void testGetUserGroupsOverLdaps() throws Exception {
+ ldapProxyBackend = new LdapProxyBackend("sslbackend", trustingConfig());
+
+ List groups = ldapProxyBackend.getUserGroups("ldaptest1", schemaManager);
+ assertTrue(groups.contains("group1"));
+ assertTrue(groups.contains("group2"));
+ }
+
+ @Test
+ public void testAuthenticateOverLdaps() throws Exception {
+ ldapProxyBackend = new LdapProxyBackend("sslbackend", trustingConfig());
+
+ Dn dn = new Dn("uid=guest,ou=people,dc=hadoop,dc=apache,dc=org");
+ assertTrue("Bind over LDAPS should succeed", ldapProxyBackend.authenticate(dn, "guest-password"));
+ assertFalse("Bind over LDAPS with wrong password should fail",
+ ldapProxyBackend.authenticate(dn, "wrong-password"));
+ }
+
+ @Test
+ public void testExplicitUseSslWithoutUrlScheme() throws Exception {
+ // Enable LDAPS via the useSsl flag rather than the ldaps:// scheme (host/port style config).
+ Map config = new HashMap<>(baseConfig());
+ config.remove("url");
+ config.put("host", "localhost");
+ config.put("port", String.valueOf(port));
+ config.put("useSsl", "true");
+ config.put("trustAllCertificates", "true");
+ ldapProxyBackend = new LdapProxyBackend("sslbackend", config);
+
+ Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
+ assertNotNull("User should be resolved over LDAPS configured via useSsl flag", entry);
+ }
+
+ @Test(expected = LdapTlsHandshakeException.class)
+ public void testUntrustedCertificateIsRejected() throws Exception {
+ // useSsl without trusting the self-signed cert: the JVM default trust store must reject it,
+ // proving certificate validation is actually enforced.
+ Map config = new HashMap<>(baseConfig());
+ config.put("trustAllCertificates", "false");
+ config.put("connectionTimeout", "5000");
+ ldapProxyBackend = new LdapProxyBackend("sslbackend", config);
+
+ ldapProxyBackend.getUser("ldaptest1", schemaManager);
+ fail("Connecting over LDAPS to a server with an untrusted certificate should fail");
+ }
+
+ private Map baseConfig() {
+ Map config = new HashMap<>();
+ config.put("baseDn", "dc=hadoop,dc=apache,dc=org");
+ config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
+ config.put("url", "ldaps://localhost:" + port);
+ config.put("systemUsername", "uid=guest,ou=people,dc=hadoop,dc=apache,dc=org");
+ config.put("systemPassword", "guest-password");
+ config.put("userSearchBase", "ou=people,dc=hadoop,dc=apache,dc=org");
+ config.put("groupSearchBase", "ou=groups,dc=hadoop,dc=apache,dc=org");
+ return config;
+ }
+
+ private Map trustingConfig() {
+ Map config = new HashMap<>(baseConfig());
+ config.put("trustAllCertificates", "true");
+ return config;
+ }
+
+ private void validateMemberOf(Entry entry, Set expectedGroups) throws Exception {
+ assertNotNull(entry.get("memberOf"));
+ assertEquals(expectedGroups.size(), entry.get("memberOf").size());
+ Set foundGroups = new HashSet<>();
+ for (Value value : entry.get("memberOf")) {
+ foundGroups.add(value.getString());
+ }
+ assertEquals(expectedGroups, foundGroups);
+ }
+
+ /**
+ * Build a keystore holding a self-signed certificate and its private key, in the JVM default
+ * keystore format (which is what ApacheDS uses to load the server certificate).
+ */
+ private static File createTempKeystore() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+ X509Certificate cert = X509CertificateUtil.generateCertificate(
+ "CN=localhost, OU=Test, O=Knox, L=Test, ST=Test, C=US", keyPair, 365, "SHA256withRSA");
+ assertNotNull("Failed to generate a self-signed test certificate", cert);
+
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+ keyStore.setKeyEntry("knox-ldaps", keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(),
+ new Certificate[] { cert });
+
+ File file = File.createTempFile("knox-ldaps-backend", ".ks");
+ file.deleteOnExit();
+ try (OutputStream os = Files.newOutputStream(file.toPath())) {
+ keyStore.store(os, KEYSTORE_PASSWORD.toCharArray());
+ }
+ return file;
+ }
+}
diff --git a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 73fcf1bfd0..a3b3d0d9a8 100644
--- a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -1335,6 +1335,26 @@ public String getLdapRolesLookupFilePath() {
return "";
}
+ @Override
+ public boolean isLDAPSSLEnabled() {
+ return false;
+ }
+
+ @Override
+ public String getLDAPSSLKeystorePath() {
+ return null;
+ }
+
+ @Override
+ public String getLDAPSSLKeystorePasswordAlias() {
+ return null;
+ }
+
+ @Override
+ public List getLDAPSSLEnabledCipherSuites() {
+ return Collections.emptyList();
+ }
+
@Override
public boolean getGroupUIServicesOnHomepage() {
return false;
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 2fdea677e5..6b36e729bf 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -151,6 +151,10 @@ public interface GatewayConfig {
String LDAP_ROLES_LOOKUP_STRATEGY = "gateway.ldap.roles.lookup.strategy";
String LDAP_ROLES_LOOKUP_REST_API_ENDPOINT = "gateway.ldap.roles.lookup.rest.api.endpoint";
String LDAP_ROLES_LOOKUP_FILE_PATH = "gateway.ldap.roles.lookup.file.path";
+ String LDAP_SSL_ENABLED = "gateway.ldap.ssl.enabled";
+ String LDAP_SSL_KEYSTORE_PATH = "gateway.ldap.ssl.keystore.path";
+ String LDAP_SSL_KEYSTORE_PASSWORD_ALIAS = "gateway.ldap.ssl.keystore.password.alias";
+ String LDAP_SSL_ENABLED_CIPHER_SUITES = "gateway.ldap.ssl.enabled.cipher.suites";
/**
* The location of the gateway configuration.
@@ -1181,6 +1185,32 @@ public interface GatewayConfig {
*/
String getLdapRolesLookupFilePath();
+ /**
+ * @return true if the embedded LDAP service should expose a secure (LDAPS) transport;
+ * otherwise false
+ */
+ boolean isLDAPSSLEnabled();
+
+ /**
+ * @return the path to the keystore holding the certificate presented by the embedded
+ * LDAP service on its secure transport. When null/blank the gateway identity keystore
+ * ({@link #getIdentityKeystorePath()}) is used.
+ */
+ String getLDAPSSLKeystorePath();
+
+ /**
+ * @return the credential-store alias for the password protecting the keystore returned by
+ * {@link #getLDAPSSLKeystorePath()}. When null/blank the gateway identity keystore password
+ * is used.
+ */
+ String getLDAPSSLKeystorePasswordAlias();
+
+ /**
+ * @return the TLS cipher suites the embedded LDAP service secure transport is restricted to,
+ * or an empty list to use the JVM defaults
+ */
+ List getLDAPSSLEnabledCipherSuites();
+
/**
* @return set of all property names in the configuration
*/
diff --git a/pom.xml b/pom.xml
index ebe25eba77..94ccc793f9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,8 +157,8 @@
17
- 2.0.0
- 2.0.0.AM26
+ 2.1.8
+ 2.0.0.AM27
2.0.0-M5
0.13
1.8.1
@@ -177,6 +177,7 @@
1.4
1.15
3.2.2
+ 4.5.0
1.26.0
2.15.0
3.2
@@ -2068,6 +2069,13 @@
commons-collections
${commons-collections.version}
+
+
+ org.apache.commons
+ commons-collections4
+ ${commons-collections4.version}
+
org.apache.commons
commons-compress
@@ -2165,6 +2173,16 @@
org.bouncycastle
bcprov-jdk15on
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+
+
+ org.bouncycastle
+ bcutil-jdk15on
+
@@ -2196,6 +2214,14 @@
org.bouncycastle
bcprov-jdk15on
+
+ org.bouncycastle
+ bcpkix-jdk15on
+
+
+ org.bouncycastle
+ bcutil-jdk15on
+
@@ -2244,6 +2270,53 @@
api-asn1-api
${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-asn1-ber
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-i18n
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-net-mina
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-aci
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-codec
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-codec-api
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-sp
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-trigger
+ ${apacheds.directory.api.version}
+
+
+ org.apache.directory.api
+ api-ldap-extras-util
+ ${apacheds.directory.api.version}
+
org.apache.mina