diff --git a/README.md b/README.md index 4b28c62..97425ae 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/npm/src/LdapEngine.js b/npm/src/LdapEngine.js index fbf7d6d..73664f5 100644 --- a/npm/src/LdapEngine.js +++ b/npm/src/LdapEngine.js @@ -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 }; @@ -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 }); @@ -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}`); diff --git a/server/.env.example b/server/.env.example index 9433023..a7e7e9c 100644 --- a/server/.env.example +++ b/server/.env.example @@ -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-----..." diff --git a/server/config/configurationLoader.js b/server/config/configurationLoader.js index 6a4def6..eba91cf 100644 --- a/server/config/configurationLoader.js +++ b/server/config/configurationLoader.js @@ -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() }; diff --git a/server/serverMain.js b/server/serverMain.js index 0da205e..0de4b85 100644 --- a/server/serverMain.js +++ b/server/serverMain.js @@ -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