Skip to content

Commit ad6c159

Browse files
authored
KNOX-3377: KNOX-3378: KNOX-3379: Support secure LDAP (LDAPS) for the Knox embedded LDAP service (#1305)
1 parent c24781a commit ad6c159

17 files changed

Lines changed: 863 additions & 11 deletions

File tree

.github/workflows/build/conf/topologies/knoxldap.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ limitations under the License.
3535
</param>
3636
<param>
3737
<name>main.ldapRealm.contextFactory.url</name>
38-
<value>ldap://localhost:33390</value> <!-- Local Knox LDAP service which is configured to proxy to the demo LDAP as a backend -->
38+
<value>ldaps://localhost:33390</value> <!-- Local Knox LDAP service (LDAPS); configured to proxy to the demo LDAP as a backend -->
3939
</param>
4040
<param>
4141
<name>main.ldapRealm.contextFactory.authenticationMechanism</name>

.github/workflows/build/gateway-site.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,20 @@ limitations under the License.
153153
<name>gateway.ldap.interceptor.names</name>
154154
<value>demoldap</value>
155155
</property>
156+
<!-- Expose the embedded Knox LDAP service over LDAPS using the dev keystore that
157+
gateway.sh provisions; its password is resolved from the credential store. -->
158+
<property>
159+
<name>gateway.ldap.ssl.enabled</name>
160+
<value>true</value>
161+
</property>
162+
<property>
163+
<name>gateway.ldap.ssl.keystore.path</name>
164+
<value>/knox-runtime/conf/ldaps-keystore.p12</value>
165+
</property>
166+
<property>
167+
<name>gateway.ldap.ssl.keystore.password.alias</name>
168+
<value>gateway_ldap_ssl_keystore_password</value>
169+
</property>
156170

157171
<!-- LDAP Backend specific configuration (proxying to demo ldap) -->
158172
<property>
@@ -165,7 +179,13 @@ limitations under the License.
165179
</property>
166180
<property>
167181
<name>gateway.ldap.interceptor.demoldap.url</name>
168-
<value>ldap://ldap:33389</value>
182+
<value>ldaps://ldap:33389</value>
183+
</property>
184+
<!-- The demo LDAP presents a self-signed dev certificate; trust it rather than
185+
wiring a truststore for this dev/CI fixture. -->
186+
<property>
187+
<name>gateway.ldap.interceptor.demoldap.trustAllCertificates</name>
188+
<value>true</value>
169189
</property>
170190
<property>
171191
<name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>

.github/workflows/build/gateway.sh

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,37 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16+
set -e
1617

18+
# The embedded Knox LDAP service is exposed over LDAPS (see gateway-site.xml:
19+
# gateway.ldap.ssl.enabled). Provision a dev-only certificate for it and make the
20+
# JVM trust that certificate so the knoxldap Shiro realm - which authenticates over
21+
# ldaps://localhost:33390 via JNDI - can validate it.
22+
KEYSTORE=/knox-runtime/conf/ldaps-keystore.p12
23+
KEYSTORE_PASSWORD=knox-ldap-ssl-password
24+
KEYSTORE_PASSWORD_ALIAS=gateway_ldap_ssl_keystore_password
1725

18-
# Start Knox
19-
java -jar /knox-runtime/bin/gateway.jar
26+
# 1) Self-signed dev certificate for the embedded LDAP service's secure transport.
27+
# Store and key passwords must match (ApacheDS opens the store and recovers the
28+
# key with a single password). PKCS12 matches the JVM default keystore type.
29+
keytool -genkeypair -alias ldaps -keyalg RSA -keysize 2048 \
30+
-dname "CN=localhost" -ext "san=dns:localhost,dns:knox" -validity 3650 \
31+
-keystore "$KEYSTORE" -storetype PKCS12 \
32+
-storepass "$KEYSTORE_PASSWORD" -keypass "$KEYSTORE_PASSWORD"
33+
34+
# 2) Store the keystore password under the alias the LDAP SSL config resolves.
35+
/knox-runtime/bin/knoxcli.sh create-alias "$KEYSTORE_PASSWORD_ALIAS" --value "$KEYSTORE_PASSWORD"
36+
37+
# 3) Trust that certificate in the JVM default truststore (cacerts) so the JNDI-based
38+
# Shiro LDAP realm accepts it. This is additive - it does not remove the default CAs.
39+
keytool -exportcert -alias ldaps -rfc \
40+
-keystore "$KEYSTORE" -storepass "$KEYSTORE_PASSWORD" -file /tmp/ldaps-cert.pem
41+
keytool -importcert -noprompt -alias knox-ldaps \
42+
-keystore "${JAVA_HOME}/lib/security/cacerts" -storepass changeit \
43+
-file /tmp/ldaps-cert.pem
44+
rm -f /tmp/ldaps-cert.pem
45+
46+
# Start Knox. Endpoint (hostname) identification is disabled for the embedded LDAPS
47+
# connection - the dev cert is self-signed - but trust is still enforced via cacerts.
48+
java -Dcom.sun.jndi.ldap.object.disableEndpointIdentification=true \
49+
-jar /knox-runtime/bin/gateway.jar

.github/workflows/build/ldap.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,22 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16-
java -jar /knox-runtime/bin/ldap.jar /knox-runtime/conf
16+
set -e
17+
18+
# Expose the demo LDAP over a secure (LDAPS) transport so the Knox backend can be
19+
# configured to proxy to it over TLS. Generate a self-signed, dev-only certificate on
20+
# each start (the demo LDAP is a throwaway fixture, not a secret). The store and key
21+
# passwords must match: ApacheDS opens the keystore and recovers the key with a single
22+
# password. PKCS12 matches the JVM default keystore type ApacheDS loads it with.
23+
KEYSTORE=/knox-runtime/conf/ldap-keystore.p12
24+
KEYSTORE_PASSWORD=ldap-keystore-password
25+
26+
keytool -genkeypair -alias ldaps -keyalg RSA -keysize 2048 \
27+
-dname "CN=ldap" -ext "san=dns:ldap,dns:localhost" -validity 3650 \
28+
-keystore "$KEYSTORE" -storetype PKCS12 \
29+
-storepass "$KEYSTORE_PASSWORD" -keypass "$KEYSTORE_PASSWORD"
30+
31+
java -Dldap.ssl.enabled=true \
32+
-Dldap.keystore.file="$KEYSTORE" \
33+
-Dldap.keystore.password="$KEYSTORE_PASSWORD" \
34+
-jar /knox-runtime/bin/ldap.jar /knox-runtime/conf

.github/workflows/tests/test_knox_ldap_proxy_search.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,25 @@
1818
These exercise the LdapProxyBackend.search() path (KNOX-3341): clients can query
1919
the embedded Knox LDAP service - which proxies to the demo LDAP backend - by
2020
objectClass, cn and uid (including wildcards), not just by a single uid lookup.
21+
22+
The connection is made over LDAPS: the embedded Knox LDAP service is configured
23+
with gateway.ldap.ssl.enabled=true, and it in turn proxies to the demo LDAP
24+
backend over LDAPS as well (gateway.ldap.interceptor.demoldap.url=ldaps://...).
25+
The gateway presents a self-signed dev certificate in CI, so certificate
26+
validation is disabled on the client side.
2127
"""
2228

2329
from __future__ import annotations
2430

2531
import os
32+
import ssl
2633
import unittest
2734
from urllib.parse import urlparse
2835

2936
import ldap3
3037

31-
# The embedded Knox LDAP service port (see gateway-site.xml: gateway.ldap.port).
38+
# The embedded Knox LDAP service secure port (see gateway-site.xml: gateway.ldap.port
39+
# with gateway.ldap.ssl.enabled=true).
3240
KNOX_LDAP_PORT = 33390
3341

3442
BASE_DN = "dc=hadoop,dc=apache,dc=org"
@@ -50,7 +58,11 @@ class TestKnoxLdapProxySearch(unittest.TestCase):
5058
"""Verify general search requests are proxied to the demo LDAP backend."""
5159

5260
def setUp(self) -> None:
53-
server = ldap3.Server(knox_host(), port=KNOX_LDAP_PORT, get_info=ldap3.NONE)
61+
# Self-signed dev certificate in CI: connect over TLS but skip validation.
62+
tls = ldap3.Tls(validate=ssl.CERT_NONE)
63+
server = ldap3.Server(
64+
knox_host(), port=KNOX_LDAP_PORT, use_ssl=True, tls=tls, get_info=ldap3.NONE
65+
)
5466
self.connection = ldap3.Connection(
5567
server, user=BIND_DN, password=BIND_PASSWORD, auto_bind=True
5668
)

gateway-demo-ldap/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@
9090
<groupId>org.bouncycastle</groupId>
9191
<artifactId>bcprov-jdk18on</artifactId>
9292
</dependency>
93+
<!-- ApacheDS 2.0.0.AM27's LdapServer.start() references bcpkix (cert utilities)
94+
even for a plaintext transport. The legacy jdk15on BouncyCastle it would pull
95+
transitively is excluded project-wide, so declare the jdk18on artifact here. -->
96+
<dependency>
97+
<groupId>org.bouncycastle</groupId>
98+
<artifactId>bcpkix-jdk18on</artifactId>
99+
</dependency>
93100
</dependencies>
94101
<build>
95102
<plugins>

gateway-demo-ldap/src/main/java/org/apache/knox/gateway/security/ldap/SimpleLdapDirectoryServer.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,24 @@ public SimpleLdapDirectoryServer( String rootDn, File usersLdif, Transport... tr
104104
server = new LdapServer();
105105
server.setTransports( transports );
106106
server.setDirectoryService( service );
107+
108+
// Optionally expose a secure (LDAPS) transport. Controlled by system properties so the
109+
// demo server can be launched in either plaintext or secure mode without code changes.
110+
if ( Boolean.parseBoolean( System.getProperty( "ldap.ssl.enabled", "false" ) ) ) {
111+
for ( Transport transport : transports ) {
112+
if ( transport instanceof TcpTransport ) {
113+
( (TcpTransport) transport ).setEnableSSL( true );
114+
}
115+
}
116+
String keystoreFile = System.getProperty( "ldap.keystore.file" );
117+
if ( keystoreFile != null && !keystoreFile.isEmpty() ) {
118+
server.setKeystoreFile( keystoreFile );
119+
}
120+
String keystorePassword = System.getProperty( "ldap.keystore.password" );
121+
if ( keystorePassword != null ) {
122+
server.setCertificatePassword( keystorePassword );
123+
}
124+
}
107125
}
108126

109127
private static void enabledPosixSchema( DirectoryService service ) throws LdapException {

gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,27 @@ public String getLdapRolesLookupFilePath() {
18631863
return get(LDAP_ROLES_LOOKUP_FILE_PATH);
18641864
}
18651865

1866+
@Override
1867+
public boolean isLDAPSSLEnabled() {
1868+
return Boolean.parseBoolean(get(LDAP_SSL_ENABLED, "false"));
1869+
}
1870+
1871+
@Override
1872+
public String getLDAPSSLKeystorePath() {
1873+
return get(LDAP_SSL_KEYSTORE_PATH, null);
1874+
}
1875+
1876+
@Override
1877+
public String getLDAPSSLKeystorePasswordAlias() {
1878+
return get(LDAP_SSL_KEYSTORE_PASSWORD_ALIAS, null);
1879+
}
1880+
1881+
@Override
1882+
public List<String> getLDAPSSLEnabledCipherSuites() {
1883+
final List<String> cipherSuites = splitConfigValueToList(LDAP_SSL_ENABLED_CIPHER_SUITES);
1884+
return cipherSuites == null ? Collections.emptyList() : cipherSuites;
1885+
}
1886+
18661887
@Override
18671888
public boolean getGroupUIServicesOnHomepage() {
18681889
return getBoolean(KNOX_HOMEPAGE_GROUP_UI_SERVICES, DEFAULT_GROUP_UI_SERVICES);

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public class KnoxLDAPServerManager {
7979
private int port;
8080
private String baseDn;
8181
private String bindUser;
82+
// Secure (LDAPS) transport configuration
83+
private boolean sslEnabled;
84+
private String sslKeystorePath;
85+
private String sslKeystorePasswordAlias;
86+
private List<String> sslEnabledCipherSuites;
8287
// Collection of DNs for the proxied backend LDAP servers
8388
private Set<String> baseDns;
8489

@@ -108,6 +113,17 @@ public void initialize(GatewayConfig config) throws Exception {
108113
this.baseDn = config.getLDAPBaseDN();
109114
this.bindUser = config.getLDAPBindUser();
110115

116+
// Secure (LDAPS) transport configuration. When enabled but no dedicated keystore is
117+
// configured, fall back to the gateway identity keystore so the embedded server can
118+
// reuse the gateway's own TLS material out of the box.
119+
this.sslEnabled = config.isLDAPSSLEnabled();
120+
if (sslEnabled) {
121+
final String configuredKeystore = config.getLDAPSSLKeystorePath();
122+
this.sslKeystorePath = StringUtils.isNotBlank(configuredKeystore) ? configuredKeystore : config.getIdentityKeystorePath();
123+
this.sslKeystorePasswordAlias = config.getLDAPSSLKeystorePasswordAlias();
124+
this.sslEnabledCipherSuites = config.getLDAPSSLEnabledCipherSuites();
125+
}
126+
111127
createInterceptors(config);
112128

113129
// Clean up previous run if it didn't shut down cleanly
@@ -215,7 +231,11 @@ public void start() throws Exception {
215231

216232
// Create LDAP server on configured port
217233
ldapServer = new LdapServer();
218-
ldapServer.setTransports(new TcpTransport(port));
234+
final TcpTransport transport = new TcpTransport(port);
235+
if (sslEnabled) {
236+
configureSsl(transport);
237+
}
238+
ldapServer.setTransports(transport);
219239
ldapServer.setDirectoryService(directoryService);
220240

221241
ldapServer.start();
@@ -280,6 +300,56 @@ private int fetchAuthenticationInterceptorIndex(final List<Interceptor> dsInterc
280300
.orElse(-1);
281301
}
282302

303+
/**
304+
* Enable the secure (LDAPS) transport on the embedded server. The keystore holding the
305+
* server certificate and its password are resolved from the gateway configuration and
306+
* credential store; the password defaults to the gateway identity keystore password when
307+
* no dedicated alias is configured. The keystore must be in the JVM default format
308+
* (see {@code java.security.KeyStore#getDefaultType()}), which is what ApacheDS uses to
309+
* load it.
310+
*/
311+
private void configureSsl(final TcpTransport transport) throws Exception {
312+
if (StringUtils.isBlank(sslKeystorePath)) {
313+
final String reason = "no keystore configured; set " + GatewayConfig.LDAP_SSL_KEYSTORE_PATH
314+
+ " or configure the gateway identity keystore";
315+
LOG.ldapSslConfigInvalid(reason);
316+
throw new IllegalStateException("Cannot enable LDAPS: " + reason);
317+
}
318+
319+
final File keystore = new File(sslKeystorePath);
320+
if (!keystore.exists()) {
321+
final String reason = "keystore file not found: " + keystore.getAbsolutePath();
322+
LOG.ldapSslConfigInvalid(reason);
323+
throw new IllegalStateException("Cannot enable LDAPS: " + reason);
324+
}
325+
326+
transport.setEnableSSL(true);
327+
ldapServer.setKeystoreFile(keystore.getAbsolutePath());
328+
329+
final char[] passwordChars = resolveSslKeystorePassword();
330+
if (passwordChars != null) {
331+
ldapServer.setCertificatePassword(new String(passwordChars));
332+
}
333+
334+
if (sslEnabledCipherSuites != null && !sslEnabledCipherSuites.isEmpty()) {
335+
ldapServer.setEnabledCipherSuites(new ArrayList<>(sslEnabledCipherSuites));
336+
}
337+
338+
LOG.ldapSslEnabled(keystore.getAbsolutePath());
339+
}
340+
341+
/**
342+
* Resolve the password protecting the LDAPS keystore from the gateway credential store.
343+
* Uses the configured alias when set, otherwise falls back to the gateway identity
344+
* keystore password (matching the keystore-path fallback in {@link #initialize(GatewayConfig)}).
345+
*/
346+
private char[] resolveSslKeystorePassword() throws Exception {
347+
if (StringUtils.isNotBlank(sslKeystorePasswordAlias)) {
348+
return aliasService.getPasswordFromAliasForGateway(sslKeystorePasswordAlias);
349+
}
350+
return aliasService.getGatewayIdentityKeystorePassword();
351+
}
352+
283353
/**
284354
* Stop the LDAP server
285355
*/

gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ public interface LdapMessages {
3333
text = "LDAP service started successfully on port {0}")
3434
void ldapServiceStarted(int port);
3535

36+
@Message(level = MessageLevel.INFO,
37+
text = "Enabling secure (LDAPS) transport for LDAP service using keystore: {0}")
38+
void ldapSslEnabled(String keystorePath);
39+
40+
@Message(level = MessageLevel.ERROR,
41+
text = "Cannot enable secure (LDAPS) transport: {0}")
42+
void ldapSslConfigInvalid(String reason);
43+
3644
@Message(level = MessageLevel.INFO,
3745
text = "Anonymous access disabled; clients must bind as: {0}")
3846
void ldapBindUserConfigured(String bindDn);

0 commit comments

Comments
 (0)