LDAP Search Filters You Actually Need
Introduction¶
Every LDAP query has a base DN, a scope, and a filter. The filter is where
people get stuck, because it's prefix notation with parentheses everywhere:
(&(objectClass=user)(mail=*)). Once the pattern clicks, you can extract
anything from a directory.
Filter building blocks¶
| Filter | Meaning |
|---|---|
(cn=jsmith) |
equality — exact match |
(cn=j*) |
substring — starts with j |
(cn=*smith*) |
substring — contains smith |
(mail=*) |
presence — attribute exists and is non-empty |
(!(mail=*)) |
negation — attribute absent |
(&(a=1)(b=2)) |
AND — both must match |
(\|(a=1)(b=2)) |
OR — either matches |
(sn~=smyth) |
approximate (soundalike) — rarely supported |
(createTimestamp>=20260101000000Z) |
greater-or-equal (lexical for strings) |
Operators go first: &, |, ! prefix the group they combine. Each
sub-condition is wrapped in its own parentheses. There is no < or >, only
>= and <=.
Special characters in values must be escaped: * → \2a, ( → \28,
) → \29, \ → \5c, NUL → \00.
Recipes¶
# All enabled people with an email address (generic LDAP)
ldapsearch -x -H ldap://dc1.example.com -D "cn=svc,ou=svc,dc=example,dc=com" -W \
-b "dc=example,dc=com" \
"(&(objectClass=inetOrgPerson)(mail=*))" cn mail
# Members of a group
ldapsearch ... -b "dc=example,dc=com" \
"(&(objectClass=group)(cn=vpn-users))" member
# Which groups is a user in? (reverse — search groups for the user's DN)
ldapsearch ... -b "dc=example,dc=com" \
"(member=cn=Jane Smith,ou=people,dc=example,dc=com)" cn
# Users created since a date
ldapsearch ... "(&(objectClass=person)(whenCreated>=20260801000000.0Z))" cn whenCreated
Active Directory: the bitwise filter¶
AD packs account state into the integer userAccountControl. To test a single
bit you need the extensible match rule 1.2.840.113556.1.4.803 (bitwise
AND):
# Disabled accounts (bit 0x2 = ACCOUNTDISABLE)
"(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))"
# ENABLED accounts (NOT that bit)
"(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
# Password never expires (0x10000 = 65536)
"(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=65536))"
Other handy AD filters:
# Locked-out accounts
"(&(objectClass=user)(lockoutTime>=1))"
# Members of a group, recursively (nested groups) — LDAP_MATCHING_RULE_IN_CHAIN
"(memberOf:1.2.840.113556.1.4.1941:=CN=All Staff,OU=Groups,DC=example,DC=com)"
# Computers that haven't logged in since a date (stale machine accounts)
"(&(objectClass=computer)(lastLogonTimestamp<=133700000000000000))"
lastLogonTimestamp is a Windows FILETIME (100 ns ticks since 1601) — convert
with w32tm or a small script; don't eyeball it.
Scope and attributes¶
ldapsearch -x ... -s base "(objectClass=*)" # just the base entry itself
ldapsearch -x ... -s one "(objectClass=ou)" # immediate children only
ldapsearch -x ... -s sub "(cn=j*)" # base + entire subtree (default)
Always list the attributes you want (cn mail memberOf) — the default returns
every attribute of every match, which is slow and noisy. Add -o ldif-wrap=no
to stop ldapsearch wrapping long lines.
Verification and troubleshooting¶
# Test a bind (authentication) separately from a search
ldapwhoami -x -H ldaps://dc1.example.com -D "[email protected]" -W
# Paged results for large result sets (AD caps at 1000 by default)
ldapsearch -x ... -E pr=500/noprompt "(objectClass=user)" cn
Size limit exceeded(LDAP result 4) — the server capped the result set (AD: 1000). Use paged results (-E pr=...) or narrow the filter.- Filter returns nothing but the entry exists — wrong base DN or scope, an
unescaped special character, or a case/spacing issue in a DN-valued filter
like
member=. Try-s subfrom the directory root to confirm the entry, then tighten. (memberOf=...)misses nested groups — plainmemberOfis direct only. Use the:1.2.840.113556.1.4.1941:chain rule (AD) or query recursively.- Bitwise filter "invalid filter" (result 87) — the OID is mistyped, or the
server isn't AD. Only AD supports
1.2.840.113556.1.4.803. Operations erroron AD without a base DN — AD requires an explicit base; it won't default to the directory root for anonymous-ish binds.- Times look wrong — generalized time is UTC (
Zsuffix); AD FILETIME attributes are a different format entirely.
Related tools and reading¶
- On-site: DNS SRV Lookup (find your domain controllers), Port Checker (389/636/3268).
- Related posts: SMB versions and signing, Writing a Bash script that won't bite you.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Network Protocols
DHCP DORA: How a Host Gets Its Address
The four DHCP packets, the renewal timers, why relays exist, and the tcpdump filter to watch it happen.
October 24, 2026SMB Versions and Signing, Demystified
SMB1 is a security liability, SMB3 encrypts and signs. Here's how dialect negotiation works and how to tell which one a connection actually used.
October 24, 2026DNS over TLS and DNS over HTTPS
Plain DNS is unencrypted and unauthenticated. DoT and DoH fix the transport — here's how each works, and the trade-off between privacy and network visibility.
October 24, 2026