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 [...]

Reply via email to