Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ AUTH_BACKENDS=ldap # mysql | mongodb | ldap | proxmox | notification | mysq
# LDAP Server Configuration
LDAP_BASE_DN=dc=company,dc=com

# Security: Require authentication for search operations
# Default: true (authentication required for security)
# Set to false only for development/testing if you need anonymous access
REQUIRE_AUTH_FOR_SEARCH=true

# MySQL configuration (for any MySQL-based system)
MYSQL_HOST=localhost
MYSQL_PORT=3306
Expand All @@ -143,6 +148,37 @@ LDAP_BIND_PASSWORD=ldap_service_password
AD_DOMAIN=company.com
```

### Security Settings

#### Require Authentication for Search

By default, authentication is required before allowing LDAP search operations:

```ini
# Authentication required by default (recommended for security)
REQUIRE_AUTH_FOR_SEARCH=true # This is the default

# Only disable for development/testing if needed
# REQUIRE_AUTH_FOR_SEARCH=false
```

**Behavior:**
- `true` (default): Clients must authenticate with valid credentials before searching
- `false`: Allows anonymous searches - only use for development/testing

**Example:**
```bash
# Without authentication (fails when REQUIRE_AUTH_FOR_SEARCH=true)
ldapsearch -H ldaps://localhost:636 -x -b "dc=company,dc=com" "(uid=john)"
# Result: Insufficient access (error 50)

# With authentication (succeeds)
ldapsearch -H ldaps://localhost:636 -x -D "uid=john,dc=company,dc=com" -w password -b "dc=company,dc=com" "(uid=john)"
# Result: Returns user information
```

**Recommendation:** Set to `true` for all production environments to prevent unauthorized directory enumeration.

### Start Service

```bash
Expand Down
28 changes: 25 additions & 3 deletions npm/src/LdapEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class LdapEngine extends EventEmitter {
port: options.port || 389,
certificate: options.certificate || null,
key: options.key || null,
requireAuthForSearch: options.requireAuthForSearch !== false,
...options
};

Expand Down Expand Up @@ -156,10 +157,10 @@ class LdapEngine extends EventEmitter {
res.end();
});

// Authenticated bind
// Authenticated bind - catch all DNs under our base
this.server.bind(this.config.baseDn, async (req, res, next) => {
const { username, password } = this._extractCredentials(req);
this.logger.debug("Authenticated bind request", { username });
this.logger.debug("Authenticated bind request", { username, dn: req.dn.toString() });

try {
this.emit('bindRequest', { username, anonymous: false });
Expand Down Expand Up @@ -193,7 +194,28 @@ class LdapEngine extends EventEmitter {
* @private
*/
_setupSearchHandlers() {
this.server.search(this.config.baseDn, async (req, res, next) => {
// Authorization middleware (if enabled)
const authorizeSearch = (req, res, next) => {
if (!this.config.requireAuthForSearch) {
return next();
}

// Check if connection has authenticated bindDN (not anonymous)
const bindDN = req.connection.ldap.bindDN;
const bindDNStr = bindDN ? bindDN.toString() : 'null';
const isAnonymous = !bindDN || bindDNStr === 'cn=anonymous';

if (isAnonymous) {
this.logger.debug(`Anonymous search rejected - authentication required`);
return next(new ldap.InsufficientAccessRightsError('Authentication required for search operations'));
}

this.logger.debug(`Authenticated search allowed for ${bindDNStr}`);
return next();
};

// Search handler with authorization middleware
this.server.search(this.config.baseDn, authorizeSearch, async (req, res, next) => {
const filterStr = req.filter.toString();
this.logger.debug(`LDAP Search - Filter: ${filterStr}, Attributes: ${req.attributes}`);

Expand Down
5 changes: 5 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ LDAP_COMMON_NAME=localhost
LDAP_BASE_DN=dc=localhost
# PORT=636

# Require authentication for search operations
# Default: true (authentication required)
# Set to false only for development/testing if you need anonymous access
# REQUIRE_AUTH_FOR_SEARCH=true

# SSL/TLS Certificate Configuration
# Option 1: Provide certificate content directly
# LDAP_CERT_CONTENT="-----BEGIN CERTIFICATE-----..."
Expand Down
1 change: 1 addition & 0 deletions server/config/configurationLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ConfigurationLoader {
bindIp: process.env.BIND_IP || '0.0.0.0',
unencrypted: process.env.LDAP_UNENCRYPTED === 'true' || process.env.LDAP_UNENCRYPTED === '1',
backendDir: process.env.BACKEND_DIR || null,
requireAuthForSearch: process.env.REQUIRE_AUTH_FOR_SEARCH !== 'false',
// Load certificates - this handles all certificate logic
...this._loadCertificates()
};
Expand Down
3 changes: 2 additions & 1 deletion server/serverMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ async function startServer(config) {
key: config.keyContent,
logger: logger,
authProviders: selectedBackends,
directoryProvider: selectedDirectory
directoryProvider: selectedDirectory,
requireAuthForSearch: config.requireAuthForSearch
});

// Set up event listeners for logging and monitoring
Expand Down