This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit 9102311f8e74facb3fb314d27b174e674b37c27d Author: Sandor Molnar <[email protected]> AuthorDate: Tue Jun 23 11:58:54 2026 +0200 KNOX-3358: Support configurable bind credentials for the embedded Knox LDAP service (#1275) (cherry picked from commit 9f945f8bd379396eb3fb49c85030fe0244fe4b19) --- .../main/resources/docker/gateway-entrypoint.sh | 8 +- .../gateway/config/impl/GatewayConfigImpl.java | 5 + .../services/factory/LdapServiceFactory.java | 1 + .../services/ldap/KnoxLDAPServerManager.java | 43 ++++++++- .../gateway/services/ldap/KnoxLDAPService.java | 10 +- .../knox/gateway/services/ldap/LdapMessages.java | 4 + .../services/ldap/KnoxLDAPServerManagerTest.java | 104 ++++++++++++++++++++- .../gateway/services/ldap/KnoxLDAPServiceTest.java | 7 ++ .../org/apache/knox/gateway/GatewayTestConfig.java | 5 + .../apache/knox/gateway/config/GatewayConfig.java | 7 ++ knox-site/docs/service_ldap_server.md | 60 +++++++++++- 11 files changed, 244 insertions(+), 10 deletions(-) diff --git a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh index 2cb0277a3..1d7eed82b 100755 --- a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh +++ b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh @@ -110,15 +110,15 @@ else fi /home/knox/knox/bin/knoxcli.sh create-master --master "${MASTER_SECRET}" -if [[ -n ${LDAP_PASSWORD_FILE} ]] -then - LDAP_BIND_PASSWORD=$(/bin/cat "${LDAP_PASSWORD_FILE}" 2> /dev/null) +# Check LDAP_BIND_PASSWORD first, and only fall back to the file if it’s unset or empty. +if [[ -z ${LDAP_BIND_PASSWORD} && -n ${LDAP_PASSWORD_FILE} ]]; then + LDAP_BIND_PASSWORD=$(/bin/cat "${LDAP_PASSWORD_FILE}" 2>/dev/null) fi -saveAlias ldap-bind-password "${LDAP_BIND_PASSWORD}" saveAlias gateway_database_user "${DATABASE_CONNECTION_USER}" saveAlias gateway_database_password "${DATABASE_CONNECTION_PASSWORD}" saveAlias gateway_database_ssl_truststore_password "${DATABASE_CONNECTION_TRUSTSTORE_PASSWORD}" +saveAlias gateway_ldap_bind_password "${LDAP_BIND_PASSWORD}" # RemoteAuthProvider truststore password saveAlias rap_truststore_password "${RAP_TRUSTSTORE_PASSWORD}" 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 a8aa19fcb..e1884db4e 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 @@ -1753,6 +1753,11 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig { return get(LDAP_BASE_DN, "dc=proxy,dc=com"); } + @Override + public String getLDAPBindUser() { + return get(LDAP_BIND_USER, null); + } + @Override public List<String> getLDAPInterceptorNames() { return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java index c68ea5d9d..fc5c57cc6 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java @@ -37,6 +37,7 @@ public class LdapServiceFactory extends AbstractServiceFactory { KnoxLDAPService service = null; if (shouldCreateService(implementation)) { service = new KnoxLDAPService(); + service.setAliasService(getAliasService(gatewayServices)); GatewayServer.registerConfigChangeListener(service); logServiceUsage(service.getClass().getName(), serviceType); } 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 197a45a41..2c2442686 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 @@ -46,6 +46,7 @@ import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory; import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory; +import org.apache.knox.gateway.services.security.AliasService; import java.io.File; import java.util.ArrayList; @@ -63,6 +64,8 @@ import java.util.stream.IntStream; */ public class KnoxLDAPServerManager { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + private static final String LDAP_BIND_PASSWORD_ALIAS = "gateway_ldap_bind_password"; + private final AliasService aliasService; @VisibleForTesting DirectoryService directoryService; @@ -72,9 +75,14 @@ public class KnoxLDAPServerManager { private File workDir; private int port; private String baseDn; + private String bindUser; // Collection of DNs for the proxied backend LDAP servers private Set<String> baseDns; + KnoxLDAPServerManager(AliasService aliasService) { + this.aliasService = aliasService; + } + /** * Initialize the LDAP server with the given configuration * @@ -90,6 +98,7 @@ public class KnoxLDAPServerManager { // Get configuration this.port = config.getLDAPPort(); this.baseDn = config.getLDAPBaseDN(); + this.bindUser = config.getLDAPBindUser(); createInterceptors(config); @@ -177,8 +186,13 @@ public class KnoxLDAPServerManager { addInterceptors(); - // Allow anonymous access - directoryService.setAllowAnonymousAccess(true); + // Require clients to bind with the configured credentials when both a bind user and + // a bind password (resolved from the gateway credential store) are set; otherwise + // keep the historical behavior of allowing anonymous access. + final char[] bindPasswordChars = aliasService.getPasswordFromAliasForGateway(LDAP_BIND_PASSWORD_ALIAS); + final String bindPassword = bindPasswordChars == null ? null : new String(bindPasswordChars); + final boolean requireBind = StringUtils.isNotBlank(bindUser) && StringUtils.isNotBlank(bindPassword); + directoryService.setAllowAnonymousAccess(!requireBind); // Start the service directoryService.startup(); @@ -186,6 +200,11 @@ public class KnoxLDAPServerManager { // Add base entries to the partitions createBaseEntries(baseDns, schemaManager); + if (requireBind) { + createBindUser(schemaManager, bindPassword); + LOG.ldapBindUserConfigured(bindUser); + } + // Create LDAP server on configured port ldapServer = new LdapServer(); ldapServer.setTransports(new TcpTransport(port)); @@ -318,6 +337,26 @@ public class KnoxLDAPServerManager { } } + /** + * Create the entry used by external clients to bind against the embedded LDAP server. + * The bind DN's parent container (e.g. {@code ou=system} or {@code ou=people,<baseDn>}) + * must already exist. The entry is added using the privileged admin session, which is + * unaffected by the anonymous-access setting. + */ + private void createBindUser(SchemaManager schemaManager, String bindPassword) throws Exception { + Dn bindDn = new Dn(schemaManager, bindUser); + if (!directoryService.getAdminSession().exists(bindDn)) { + String rdnValue = bindDn.getRdn().getValue(); + Entry bindEntry = new DefaultEntry(schemaManager); + bindEntry.setDn(bindDn); + bindEntry.add("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson"); + bindEntry.add("cn", rdnValue); + bindEntry.add("sn", rdnValue); + bindEntry.add("userPassword", bindPassword); + directoryService.getAdminSession().add(bindEntry); + } + } + public int getPort() { return port; } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java index 0859935c1..b8a919c62 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java @@ -22,6 +22,7 @@ import org.apache.knox.gateway.config.GatewayConfigChangeListener; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.Service; import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.AliasService; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ public class KnoxLDAPService implements Service, GatewayConfigChangeListener { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); KnoxLDAPServerManager ldapServerManager; + AliasService aliasService; private boolean enabled; @Override @@ -46,13 +48,17 @@ public class KnoxLDAPService implements Service, GatewayConfigChangeListener { try { // Initialize the LDAP server manager with configuration - ldapServerManager = new KnoxLDAPServerManager(); + ldapServerManager = new KnoxLDAPServerManager(aliasService); ldapServerManager.initialize(config); } catch (Exception e) { throw new ServiceLifecycleException("Failed to initialize LDAP service", e); } } + public void setAliasService(AliasService aliasService) { + this.aliasService = aliasService; + } + @Override public void start() throws ServiceLifecycleException { if (!enabled) { @@ -89,7 +95,7 @@ public class KnoxLDAPService implements Service, GatewayConfigChangeListener { this.enabled = config.isLDAPEnabled(); if (this.enabled) { - this.ldapServerManager = this.ldapServerManager == null ? new KnoxLDAPServerManager() : this.ldapServerManager; + this.ldapServerManager = this.ldapServerManager == null ? new KnoxLDAPServerManager(aliasService) : this.ldapServerManager; ldapServerManager.stop(); ldapServerManager.initialize(config); ldapServerManager.start(); 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 506bc6ba3..313624e20 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,10 @@ public interface LdapMessages { text = "LDAP service started successfully on port {0}") void ldapServiceStarted(int port); + @Message(level = MessageLevel.INFO, + text = "Anonymous access disabled; clients must bind as: {0}") + void ldapBindUserConfigured(String bindDn); + @Message(level = MessageLevel.INFO, text = "Stopping LDAP service on port {0}") void ldapServiceStopping(int port); 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 77229054e..8fc51bcbe 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 @@ -18,9 +18,16 @@ package org.apache.knox.gateway.services.ldap; import org.apache.directory.api.ldap.codec.api.ControlFactory; +import org.apache.directory.api.ldap.model.cursor.EntryCursor; +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.message.Control; +import org.apache.directory.api.ldap.model.message.SearchScope; +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.config.GatewayConfig; +import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory; import org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants; import org.easymock.EasyMock; @@ -51,6 +58,9 @@ import static org.junit.Assert.assertFalse; */ public class KnoxLDAPServerManagerTest { + private static final String BIND_DN = "uid=knox,ou=system"; + private static final String BIND_PASSWORD = "knox-password"; + private KnoxLDAPServerManager serverManager; private File tempWorkDir; private File tempLdapFile; @@ -58,7 +68,10 @@ public class KnoxLDAPServerManagerTest { @Before public void setUp() throws Exception { - serverManager = new KnoxLDAPServerManager(); + // By default no bind password is stored in the credential store, so the server + // runs with anonymous access (the historical behavior). Bind-enforcing tests + // rebuild the manager via useBindPassword(...). + serverManager = new KnoxLDAPServerManager(aliasServiceReturning(null)); // Create temporary work directory tempWorkDir = File.createTempFile("knox-ldap-work", ""); @@ -395,6 +408,95 @@ public class KnoxLDAPServerManagerTest { assertTrue(controlFactoryMap.get(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID) instanceof RolesLookupBypassControlFactory); } + @Test(expected = LdapException.class) + public void testBindRequiredRejectsAnonymous() throws Exception { + useBindPassword(BIND_PASSWORD); + serverManager.initialize(createBindEnabledConfig()); + serverManager.start(); + + // Anonymous access is disabled, so an anonymous bind must be rejected. + try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) { + connection.bind(); + } + } + + @Test + public void testBindWithConfiguredCredentialsSucceeds() throws Exception { + useBindPassword(BIND_PASSWORD); + serverManager.initialize(createBindEnabledConfig()); + serverManager.start(); + + try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) { + connection.bind(BIND_DN, BIND_PASSWORD); + assertTrue("Connection should be authenticated", connection.isAuthenticated()); + // An authenticated client should be able to search. + try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) { + assertTrue("Authenticated search should return at least one entry", cursor.next()); + } + } + } + + @Test(expected = LdapAuthenticationException.class) + public void testWrongBindPasswordRejected() throws Exception { + useBindPassword(BIND_PASSWORD); + serverManager.initialize(createBindEnabledConfig()); + serverManager.start(); + + try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) { + connection.bind(BIND_DN, "wrong-password"); + } + } + + @Test + public void testAnonymousStillAllowedWhenUnconfigured() throws Exception { + 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(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + serverManager.start(); + + // No bind credentials configured -> anonymous access remains allowed (backward compatible). + try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) { + connection.bind(); + try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) { + assertTrue("Anonymous search should return at least one entry", cursor.next()); + } + } + } + + private GatewayConfig createBindEnabledConfig() { + 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.getLDAPBindUser()).andReturn(BIND_DN).anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); + expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes(); + replay(mockConfig); + return mockConfig; + } + + /** Rebuild the server manager so its credential store resolves the bind password to the given value. */ + private void useBindPassword(String password) throws Exception { + serverManager = new KnoxLDAPServerManager(aliasServiceReturning(password)); + } + + /** Create an AliasService whose gateway password lookups return the given value (null => alias not set). */ + private AliasService aliasServiceReturning(String password) throws Exception { + AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())) + .andReturn(password == null ? null : password.toCharArray()).anyTimes(); + replay(aliasService); + return aliasService; + } + private Map<String, String> createFileBackendInterceptorConfig() { Map<String, String> config = new HashMap<>(); config.put("interceptorType", "backend"); 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 530cdb102..88d215c76 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 @@ -19,6 +19,7 @@ package org.apache.knox.gateway.services.ldap; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.AliasService; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -29,6 +30,7 @@ import java.util.List; import java.util.Map; import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; @@ -49,6 +51,10 @@ public class KnoxLDAPServiceTest { @Before public void setUp() throws Exception { ldapService = new KnoxLDAPService(); + // No bind password stored in the credential store -> anonymous access (default behavior). + final AliasService aliasService = createNiceMock(AliasService.class); + replay(aliasService); + ldapService.setAliasService(aliasService); mockConfig = createMock(GatewayConfig.class); // Create temporary directories and files @@ -170,6 +176,7 @@ public class KnoxLDAPServiceTest { expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce(); expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes(); expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ? "dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce(); + expect(mockConfig.getLDAPBindUser()).andReturn(null).anyTimes(); expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce(); expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce(); replay(mockConfig); 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 5331f2bc2..1fd140290 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 @@ -1245,6 +1245,11 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig { return "dc=test,dc=com"; } + @Override + public String getLDAPBindUser() { + return null; + } + @Override public List<String> getLDAPInterceptorNames() { return List.of("testinterceptor"); 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 0eac12d0d..065952d96 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 @@ -128,6 +128,7 @@ public interface GatewayConfig { String LDAP_ENABLED = "gateway.ldap.enabled"; String LDAP_PORT = "gateway.ldap.port"; String LDAP_BASE_DN = "gateway.ldap.base.dn"; + String LDAP_BIND_USER = "gateway.ldap.bind.user"; String LDAP_INTERCEPTOR_NAMES = "gateway.ldap.interceptor.names"; String LDAP_BACKEND_DATA_FILE = "gateway.ldap.backend.data.file"; String LDAP_RECURSIVE_GROUP_RESOLUTION = "gateway.ldap.recursive.group.resolution"; @@ -1072,6 +1073,12 @@ public interface GatewayConfig { */ String getLDAPBaseDN(); + /** + * @return the bind DN required to query the embedded LDAP service, or null/blank if + * anonymous access should be allowed + */ + String getLDAPBindUser(); + /** * @return the list of interceptor names for LDAP server */ diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md index c6e6e9128..0d4a482c4 100644 --- a/knox-site/docs/service_ldap_server.md +++ b/knox-site/docs/service_ldap_server.md @@ -39,11 +39,69 @@ The service is configured in `gateway-site.xml`. | `gateway.ldap.enabled` | `false` | Enables or disables the embedded LDAP service. | | `gateway.ldap.port` | `3890` | The port on which the LDAP server listens. | | `gateway.ldap.base.dn` | `dc=proxy,dc=com` | The base DN for the LDAP server. | +| `gateway.ldap.bind.user` | N/A | Full bind DN (e.g. `uid=knox,ou=system`) that clients must authenticate as. When set together with the `gateway_ldap_bind_password` credential store alias, anonymous access is disabled. The bind DN's parent container must already exist (`ou=system` always exists; `ou=people,{base.dn}` and `ou=groups,{base.dn}` are created automatically). | | `gateway.ldap.interceptor.names` | N/A | A comma separated list of interceptors to use. A separate interceptor configuration block will be used for each name. | | `gateway.ldap.roles.lookup.strategy` | N/A | The LDAP roles lookup strategy (`file` or `rest`). | | `gateway.ldap.roles.lookup.rest.api.endpoint` | N/A | The LDAP roles lookup REST API endpoint. | | `gateway.ldap.roles.lookup.file.path` | N/A | The LDAP roles lookup file path. | +### Bind Credentials + +By default the embedded LDAP server permits anonymous access. To require clients to +authenticate, set `gateway.ldap.bind.user` and store the matching password in the gateway +credential store under the `gateway_ldap_bind_password` alias. When both are present, +anonymous access is disabled and clients must bind with these credentials. + +#### Relationship between the base DN and the bind user + +`gateway.ldap.bind.user` is a **full DN**, not a bare username — `admin` on its own is not +valid. The bind entry is created inside the embedded directory at start-up, so its parent +container must already exist. The server creates the following containers under the +configured `gateway.ldap.base.dn`: + +- `ou=people,{gateway.ldap.base.dn}` +- `ou=groups,{gateway.ldap.base.dn}` + +(`ou=system` also always exists, independently of the base DN.) The bind DN must therefore +be placed under one of these containers; a DN under a container that is not created (e.g. +`ou=admins`) will fail. + +For example, with: + +```xml +<property> + <name>gateway.ldap.base.dn</name> + <value>dc=hadoop,dc=apache,dc=org</value> +</property> +``` + +a good bind user is a dedicated service identity under the auto-created `ou=people` +container — note how the base DN is the suffix of the bind DN: + +```xml +<property> + <name>gateway.ldap.bind.user</name> + <value>uid=knox,ou=people,dc=hadoop,dc=apache,dc=org</value> +</property> +``` + +and the password is stored as: + +```shell script +knoxcli.sh create-alias gateway_ldap_bind_password --value knoxsecret +``` + +Clients then bind with the full DN, e.g.: + +```shell script +ldapsearch -x -H ldap://localhost:3890 -D "uid=knox,ou=people,dc=hadoop,dc=apache,dc=org" -w knoxsecret -b "" "(uid=admin)" +``` + +The bind entry is created as an `inetOrgPerson`, so either a `uid`-based RDN +(`uid=knox,...`) or a `cn`-based RDN (`cn=bind,...`) is valid. Use a dedicated identity +(e.g. `uid=knox`) rather than the built-in ApacheDS admin `uid=admin,ou=system`, which +remains available with its default credentials. + ### Interceptor Types #### Common Interceptor Properties @@ -296,4 +354,4 @@ Alternative: Use host and port instead of URL - **Logs**: LDAP service logs can be found in `gateway.log`. Look for messages from `org.apache.knox.gateway.services.ldap`. - **Lock Files**: If Knox crashes, an `instance.lock` file might remain in `${GATEWAY_DATA_HOME}/ldap-server/run/`. The service attempts to clean this up on startup. -- **Anonymous Access**: The embedded LDAP server allows anonymous access by default to facilitate discovery and simple binds, but backend lookups are performed using the configured `systemUsername`. +- **Anonymous Access**: The embedded LDAP server allows anonymous access by default to facilitate discovery and simple binds, but backend lookups are performed using the configured `systemUsername`. To require authentication, set `gateway.ldap.bind.user` and store the corresponding password in the gateway credential store under the `gateway_ldap_bind_password` alias (e.g. `knoxcli.sh create-alias gateway_ldap_bind_password --value <password>`). Clients must then bind with those credentia [...]
