Skip to main content

LDAP Protocol: Directory Services 2026 — Complete Guide with Python, OpenLDAP, and Security

Created: March 11, 2026 Larry Qu 7 min read

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

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 value
  • MODIFY_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

Comments

👍 Was this article helpful?