This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new ad6c15958 KNOX-3377: KNOX-3378: KNOX-3379: Support secure LDAP (LDAPS) 
for the Knox embedded LDAP service (#1305)
ad6c15958 is described below

commit ad6c15958ba2ba3e78a5fd5225a43327003fa288
Author: Sandor Molnar <[email protected]>
AuthorDate: Wed Jul 15 10:47:35 2026 +0200

    KNOX-3377: KNOX-3378: KNOX-3379: Support secure LDAP (LDAPS) for the Knox 
embedded LDAP service (#1305)
---
 .../workflows/build/conf/topologies/knoxldap.xml   |   2 +-
 .github/workflows/build/gateway-site.xml           |  22 +-
 .github/workflows/build/gateway.sh                 |  34 ++-
 .github/workflows/build/ldap.sh                    |  20 +-
 .../workflows/tests/test_knox_ldap_proxy_search.py |  16 +-
 gateway-demo-ldap/pom.xml                          |   7 +
 .../security/ldap/SimpleLdapDirectoryServer.java   |  18 ++
 .../gateway/config/impl/GatewayConfigImpl.java     |  21 ++
 .../services/ldap/KnoxLDAPServerManager.java       |  72 +++++-
 .../knox/gateway/services/ldap/LdapMessages.java   |   8 +
 .../services/ldap/backend/LdapProxyBackend.java    | 131 +++++++++-
 .../services/ldap/KnoxLDAPServerManagerTest.java   | 132 +++++++++++
 .../gateway/services/ldap/KnoxLDAPServiceTest.java |   1 +
 .../ldap/backend/LdapProxyBackendSslTest.java      | 263 +++++++++++++++++++++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  20 ++
 .../apache/knox/gateway/config/GatewayConfig.java  |  30 +++
 pom.xml                                            |  77 +++++-
 17 files changed, 863 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/build/conf/topologies/knoxldap.xml 
b/.github/workflows/build/conf/topologies/knoxldap.xml
index 884e8bc5e..3b886b777 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.
                        </param>
                        <param>
                                <name>main.ldapRealm.contextFactory.url</name>
-                               <value>ldap://localhost:33390</value>  <!-- 
Local Knox LDAP service which is configured to proxy to the demo LDAP as a 
backend -->
+                               <value>ldaps://localhost:33390</value>  <!-- 
Local Knox LDAP service (LDAPS); configured to proxy to the demo LDAP as a 
backend -->
                        </param>
                        <param>
                                
<name>main.ldapRealm.contextFactory.authenticationMechanism</name>
diff --git a/.github/workflows/build/gateway-site.xml 
b/.github/workflows/build/gateway-site.xml
index 8f35a939f..36e884a3e 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -153,6 +153,20 @@ limitations under the License.
         <name>gateway.ldap.interceptor.names</name>
         <value>demoldap</value>
     </property>
+    <!-- Expose the embedded Knox LDAP service over LDAPS using the dev 
keystore that
+         gateway.sh provisions; its password is resolved from the credential 
store. -->
+    <property>
+        <name>gateway.ldap.ssl.enabled</name>
+        <value>true</value>
+    </property>
+    <property>
+        <name>gateway.ldap.ssl.keystore.path</name>
+        <value>/knox-runtime/conf/ldaps-keystore.p12</value>
+    </property>
+    <property>
+        <name>gateway.ldap.ssl.keystore.password.alias</name>
+        <value>gateway_ldap_ssl_keystore_password</value>
+    </property>
 
     <!-- LDAP Backend specific configuration (proxying to demo ldap) -->
     <property>
@@ -165,7 +179,13 @@ limitations under the License.
     </property>
     <property>
         <name>gateway.ldap.interceptor.demoldap.url</name>
-        <value>ldap://ldap:33389</value>
+        <value>ldaps://ldap:33389</value>
+    </property>
+    <!-- The demo LDAP presents a self-signed dev certificate; trust it rather 
than
+         wiring a truststore for this dev/CI fixture. -->
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.trustAllCertificates</name>
+        <value>true</value>
     </property>
     <property>
         <name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>
diff --git a/.github/workflows/build/gateway.sh 
b/.github/workflows/build/gateway.sh
index f31dd441c..e4927b7b2 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 ad3d9c8e3..fda7eaa92 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 9d015551e..bb42336ad 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 96afc9980..55eea594f 100644
--- a/gateway-demo-ldap/pom.xml
+++ b/gateway-demo-ldap/pom.xml
@@ -90,6 +90,13 @@
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk18on</artifactId>
         </dependency>
+        <!-- ApacheDS 2.0.0.AM27's LdapServer.start() references bcpkix (cert 
utilities)
+             even for a plaintext transport. The legacy jdk15on BouncyCastle 
it would pull
+             transitively is excluded project-wide, so declare the jdk18on 
artifact here. -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk18on</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
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 1561e325e..f3e562419 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 class SimpleLdapDirectoryServer {
     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 d401f3080..061518537 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 class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
     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<String> getLDAPSSLEnabledCipherSuites() {
+    final List<String> 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 5d41d7710..4f30fa80c 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<String> sslEnabledCipherSuites;
     // Collection of DNs for the proxied backend LDAP servers
     private Set<String> baseDns;
 
@@ -108,6 +113,17 @@ public class KnoxLDAPServerManager {
         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 class KnoxLDAPServerManager {
 
         // 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 @@ public class KnoxLDAPServerManager {
                 .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 a69ad7cd8..816fef003 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 300acff39..8466f5b12 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.directory.ldap.client.api.ValidatingPoolableLdapConnectionFact
 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 class LdapProxyBackend implements LdapBackend {
         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 @@ public class LdapProxyBackend implements LdapBackend {
         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 @@ public class LdapProxyBackend implements LdapBackend {
         }
     }
 
+    /**
+     * 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:
+     * <ul>
+     *   <li>{@code trustAllCertificates=true} disables validation (test/dev 
only);</li>
+     *   <li>a configured {@code trustStore} is loaded and used as the trust 
anchor;</li>
+     *   <li>otherwise the JVM default trust store is used.</li>
+     * </ul>
+     */
+    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 class LdapProxyBackend implements LdapBackend {
         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 b277cb835..78631552c 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.DefaultEntry;
 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.Test;
 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.assertEquals;
 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 class KnoxLDAPServerManagerTest {
         }
     }
 
+    @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 @@ public class KnoxLDAPServerManagerTest {
         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 88d215c76..ce681a561 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 class KnoxLDAPServiceTest {
 
     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 000000000..96b8e5a8f
--- /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<String> 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<String, String> 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<String, String> 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<String, String> baseConfig() {
+        Map<String, String> 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<String, String> trustingConfig() {
+        Map<String, String> config = new HashMap<>(baseConfig());
+        config.put("trustAllCertificates", "true");
+        return config;
+    }
+
+    private void validateMemberOf(Entry entry, Set<String> expectedGroups) 
throws Exception {
+        assertNotNull(entry.get("memberOf"));
+        assertEquals(expectedGroups.size(), entry.get("memberOf").size());
+        Set<String> 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 73fcf1bfd..a3b3d0d9a 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 class GatewayTestConfig extends Configuration 
implements GatewayConfig {
     return "";
   }
 
+  @Override
+  public boolean isLDAPSSLEnabled() {
+    return false;
+  }
+
+  @Override
+  public String getLDAPSSLKeystorePath() {
+    return null;
+  }
+
+  @Override
+  public String getLDAPSSLKeystorePasswordAlias() {
+    return null;
+  }
+
+  @Override
+  public List<String> 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 2fdea677e..6b36e729b 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<String> getLDAPSSLEnabledCipherSuites();
+
   /**
    * @return set of all property names in the configuration
    */
diff --git a/pom.xml b/pom.xml
index ebe25eba7..94ccc793f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,8 +157,8 @@
         <maven.compiler.target>17</maven.compiler.target>
 
         <!-- Dependencies sorted alphabetically -->
-        <apacheds.directory.api.version>2.0.0</apacheds.directory.api.version>
-        
<apacheds.directory.server.version>2.0.0.AM26</apacheds.directory.server.version>
+        <apacheds.directory.api.version>2.1.8</apacheds.directory.api.version>
+        
<apacheds.directory.server.version>2.0.0.AM27</apacheds.directory.server.version>
         <apacheds-jdbm.version>2.0.0-M5</apacheds-jdbm.version>
         <apache-rat-plugin.version>0.13</apache-rat-plugin.version>
         <ant-nodeps.version>1.8.1</ant-nodeps.version>
@@ -177,6 +177,7 @@
         <commons-cli.version>1.4</commons-cli.version>
         <commons-codec.version>1.15</commons-codec.version>
         <commons-collections.version>3.2.2</commons-collections.version>
+        <commons-collections4.version>4.5.0</commons-collections4.version>
         <commons-compress.version>1.26.0</commons-compress.version>
         <commons-configuration2.version>2.15.0</commons-configuration2.version>
         <commons-digester3.version>3.2</commons-digester3.version>
@@ -2068,6 +2069,13 @@
                 <artifactId>commons-collections</artifactId>
                 <version>${commons-collections.version}</version>
             </dependency>
+            <dependency>
+                <!-- Converge commons-collections4: ApacheDS pulls 4.4 
transitively while
+                     directory-api 2.1.8 pulls 4.5.0. Pin to the newer, 
backward-compatible 4.5.0. -->
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-collections4</artifactId>
+                <version>${commons-collections4.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.commons</groupId>
                 <artifactId>commons-compress</artifactId>
@@ -2165,6 +2173,16 @@
                         <groupId>org.bouncycastle</groupId>
                         <artifactId>bcprov-jdk15on</artifactId>
                     </exclusion>
+                    <!-- ApacheDS 2.0.0.AM27 pulls the legacy jdk15on 
BouncyCastle; Knox
+                         standardizes on the jdk18on artifacts (same 
packages). -->
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcpkix-jdk15on</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcutil-jdk15on</artifactId>
+                    </exclusion>
                 </exclusions>
             </dependency>
             <dependency>
@@ -2196,6 +2214,14 @@
                         <groupId>org.bouncycastle</groupId>
                         <artifactId>bcprov-jdk15on</artifactId>
                     </exclusion>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcpkix-jdk15on</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcutil-jdk15on</artifactId>
+                    </exclusion>
                 </exclusions>
             </dependency>
             <dependency>
@@ -2244,6 +2270,53 @@
                 <artifactId>api-asn1-api</artifactId>
                 <version>${apacheds.directory.api.version}</version>
             </dependency>
+            <!-- Pin the remaining directory-api artifacts (pulled 
transitively by ApacheDS) to
+                 the same version to avoid a mixed directory-api classpath. -->
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-asn1-ber</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-i18n</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-net-mina</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-aci</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-codec</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-codec-api</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-sp</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-trigger</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.directory.api</groupId>
+                <artifactId>api-ldap-extras-util</artifactId>
+                <version>${apacheds.directory.api.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>org.apache.mina</groupId>

Reply via email to