Introduction
LDAP (Lightweight Directory Access Protocol) is the backbone of enterprise identity management — it provides a standardized way to access and manage directory information including user accounts, groups, devices, and organizational structure. Applications and operating systems use LDAP for authentication (via PAM — Pluggable Authentication Modules), authorization (group membership lookup), and address book services.
This guide covers the LDAP directory information tree (DIT) structure, Python ldap3 code with explained parameters for search, add, modify, and delete operations, OpenLDAP slapd server configuration on Ubuntu 24.04, LDAP search filter syntax reference, TLS and access control hardening, and integration with Linux authentication via SSSD (System Security Services Daemon).
Directory Structure (DIT)
LDAP directories organize entries in a tree structure called the Directory Information Tree (DIT). Each entry has a Distinguished Name (DN) that uniquely identifies it in the tree:
flowchart TD
root["dc=example,dc=com"]
root --> ou1["ou=users"]
ou1 --> u1["uid=john<br/>cn: John Doe<br/>mail: [email protected]"]
ou1 --> u2["uid=jane<br/>cn: Jane Smith<br/>mail: [email protected]"]
ou1 --> u3["uid=bob<br/>cn: Bob Wilson<br/>mail: [email protected]"]
root --> ou2["ou=groups"]
ou2 --> g1["cn=admins<br/>memberUid: john, bob"]
ou2 --> g2["cn=developers<br/>memberUid: jane, bob"]
root --> ou3["ou=servers"]
ou3 --> s1["cn=web-01<br/>ipHostNumber: 10.0.1.10"]
ou3 --> s2["cn=db-01<br/>ipHostNumber: 10.0.1.20"]
Each entry consists of attribute-value pairs defined by an object class. The dn attribute is the entry’s unique path in the tree (e.g., uid=john,ou=users,dc=example,dc=com).
Common LDAP Attributes
| Attribute | Description | Example |
|---|---|---|
dc |
Domain Component | example |
ou |
Organizational Unit | users |
cn |
Common Name | John Doe |
uid |
User ID | john |
sn |
Surname | Doe |
givenName |
First Name | John |
mail |
[email protected] |
|
userPassword |
Password hash | {SSHA}... |
memberOf |
Group membership DN | cn=admins,ou=groups,dc=example,dc=com |
uidNumber |
Unix UID | 1001 |
gidNumber |
Unix GID | 100 |
homeDirectory |
Home path | /home/john |
loginShell |
Shell | /bin/bash |
OpenLDAP Server Configuration
Install and configure OpenLDAP on Ubuntu 24.04. OpenLDAP 2.6.13 LTS (released March 2026) is the current stable release and is available in Ubuntu 24.04 repositories:
sudo apt update
sudo apt install slapd ldap-utils
# During install, set admin password for cn=admin,dc=example,dc=com
Add Base Structure via LDIF
# base.ldif — initial directory structure
dn: dc=example,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: Example Corp
dc: example
dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
ou: users
dn: ou=groups,dc=example,dc=com
objectClass: organizationalUnit
ou: groups
dn: ou=servers,dc=example,dc=com
objectClass: organizationalUnit
ou: servers
# Load the base structure
sudo ldapadd -x -D cn=admin,dc=example,dc=com -W -f base.ldif
Add a User and Group
# user-john.ldif
dn: uid=john,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: john
cn: John Doe
sn: Doe
mail: [email protected]
uidNumber: 1001
gidNumber: 100
homeDirectory: /home/john
loginShell: /bin/bash
userPassword: {SSHA}encrypted-password-hash-here
dn: cn=developers,ou=groups,dc=example,dc=com
objectClass: posixGroup
cn: developers
gidNumber: 100
memberUid: john
Generate the password hash:
slappasswd -h {SSHA} -s MySecurePassword
# {SSHA}5k7M...
Python LDAP Client with ldap3
The ldap3 library provides a modern Python API for LDAP operations.
Connect and Search
import ldap3
# Connect to the LDAP server and bind as admin
server = ldap3.Server('ldap://ldap.example.com')
conn = ldap3.Connection(
server,
user='cn=admin,dc=example,dc=com',
password='admin-password'
)
conn.bind()
# Search for all person entries under ou=users
# search_base: the starting point in the DIT
# search_filter: LDAP filter string
# attributes: which attributes to return
conn.search(
search_base='ou=users,dc=example,dc=com',
search_filter='(objectClass=person)',
attributes=['cn', 'mail', 'uid', 'uidNumber']
)
# conn.entries contains the results
for entry in conn.entries:
print(f"{entry.cn} ({entry.mail}) — UID: {entry.uidNumber}")
# John Doe ([email protected]) — UID: 1001
Add an Entry
conn.add(
'uid=newuser,ou=users,dc=example,dc=com', # DN of the new entry
'inetOrgPerson', # Object class
{ # Attribute-value pairs
'cn': 'New User',
'sn': 'User',
'mail': '[email protected]',
'uid': 'newuser',
'userPassword': 'temporary-password'
}
)
The add method creates a new entry at the specified DN. All attributes required by the object class (inetOrgPerson requires cn and sn) must be provided.
Modify an Entry
conn.modify(
'uid=john,ou=users,dc=example,dc=com',
{
'mail': [(ldap3.MODIFY_REPLACE, '[email protected]')],
'description': [(ldap3.MODIFY_ADD, 'Updated contact info')]
}
)
Modify operations use lists of (change_type, value) tuples:
MODIFY_REPLACE: Replace existing valueMODIFY_ADD: Add a value (to multi-valued attributes)MODIFY_DELETE: Remove a value
Delete an Entry
conn.delete('uid=newuser,ou=users,dc=example,dc=com')
Authenticate a User (Bind)
LDAP’s most common use case: verify a user’s password by attempting to bind as that user:
def authenticate_user(username, password):
"""Verify a user's password by binding as that user.
Returns True if the bind succeeds (password is correct),
False otherwise.
"""
user_dn = f'uid={username},ou=users,dc=example,dc=com'
conn = ldap3.Connection(server, user=user_dn, password=password)
return conn.bind() # Returns True if credentials are valid
LDAP Search Filter Reference
| Filter | Description | Example |
|---|---|---|
(objectClass=person) |
All person entries | (objectClass=inetOrgPerson) |
(uid=joh*) |
Wildcard (starts with) | (cn=*Smith*) |
(&(objClass=person)(uidNumber>1000)) |
AND | Active users |
| `( | (mail=john@)(mail=jane@))` | OR |
(!(uid=admin)) |
NOT | Exclude admin |
(memberOf=cn=admins,ou=groups,dc=example,dc=com) |
Group membership | Admin group users |
(&(objClass=posixAccount)(uidNumber>=1000)) |
AND + range | All regular unix users |
LDAP Security Hardening
Enable TLS
# Generate self-signed certificate (use Let's Encrypt in production)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ldap/sasl2/server.key \
-out /etc/ldap/sasl2/server.crt \
-subj "/CN=ldap.example.com"
sudo chown openldap:openldap /etc/ldap/sasl2/server.key
sudo chmod 640 /etc/ldap/sasl2/server.key
# Enable TLS in OpenLDAP
sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/sasl2/server.crt
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/sasl2/server.crt
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/sasl2/server.key
EOF
sudo systemctl restart slapd
Enforce LDAP Signing and Channel Binding
LDAP signing cryptographically signs every LDAP message to prevent tampering and replay attacks. Channel binding ties the authentication session to the underlying TLS connection using a Channel Binding Token (CBT), preventing relay attacks even over encrypted channels.
Windows Server 2025 now requires LDAP signing by default for new Active Directory deployments, and sets channel binding to When supported. This is a significant hardening improvement over earlier versions where both were optional. The critical CVE-2025-54918 (September 2025, CVSS 8.8) demonstrated that attackers with a low-privileged domain account can escalate to SYSTEM on domain controllers when LDAP signing and channel binding are not enforced — by combining NTLM relay with authentication coercion via the Print Spooler service.
Check current settings on a Windows domain controller:
# Check LDAP signing policy (0=None, 1=Negotiate, 2=Required)
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" \
-Name "LDAPServerIntegrity"
# Check channel binding policy (0=Disabled, 1=When supported, 2=Always)
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" \
-Name "LdapEnforceChannelBinding"
Enforce LDAP signing and channel binding via Group Policy or registry:
# Require LDAP signing (value 2)
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" \
-Name "LDAPServerIntegrity" -Value 2 -Type DWord
# Always require channel binding (value 2)
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" \
-Name "LdapEnforceChannelBinding" -Value 2 -Type DWord
On OpenLDAP, enforce TLS-only connections by setting olcTLSRequired and disabling cleartext LDAP on port 389. Blocking unsigned binds is achieved through access control rules that reject simple binds over non-TLS connections.
Configure Access Controls
# Restrict password visibility to the owning user and admin
sudo ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcAccess
olcAccess: to attrs=userPassword
by self write
by anonymous auth
by dn.base="cn=admin,dc=example,dc=com" write
by * none
-
add: olcAccess
olcAccess: to dn.base=""
by * read
-
add: olcAccess
olcAccess: to *
by self read
by dn.base="cn=admin,dc=example,dc=com" write
by users read
by * none
EOF
Linux Authentication via LDAP
SSSD (System Security Services Daemon) bridges LDAP directories with Linux system auth — it handles user/group lookups via NSS and authentication via PAM, caching credentials locally to reduce LDAP server load.
# Install SSSD for LDAP authentication
sudo apt install sssd-ldap
# Configure /etc/sssd/sssd.conf
sudo tee /etc/sssd/sssd.conf << 'EOF'
[sssd]
services = nss, pam
domains = example.com
[domain/example.com]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://ldap.example.com
ldap_search_base = dc=example,dc=com
ldap_user_search_base = ou=users,dc=example,dc=com
ldap_group_search_base = ou=groups,dc=example,dc=com
ldap_user_object_class = posixAccount
ldap_group_object_class = posixGroup
enumerate = false
cache_credentials = true
EOF
sudo chmod 600 /etc/sssd/sssd.conf
sudo systemctl restart sssd
# Test authentication
getent passwd john
# john:*:1001:100:John Doe:/home/john:/bin/bash
Resources
- RFC 4511 — Lightweight Directory Access Protocol
- OpenLDAP Admin Guide — slapd configuration reference
- ldap3 Python Library Documentation — Full API reference
- SSSD LDAP Provider — Linux LDAP authentication
- LDAP Filter Syntax (RFC 4515) — Search filter reference
- LDAP Signing and Channel Binding for AD DS — Microsoft’s guide to LDAP hardening (Windows Server 2025+)
- CVE-2025-54918: NTLM LDAP Authentication Bypass — Critical LDAP relay vulnerability analysis
- OpenLDAP Release Roadmap — OpenLDAP 2.6 LTS and 2.7 feature release plans
Comments