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