This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 9f945f8bd KNOX-3358: Support configurable bind credentials for the
embedded Knox LDAP service (#1275)
9f945f8bd is described below
commit 9f945f8bd379396eb3fb49c85030fe0244fe4b19
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)
---
.../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 [...]