Server

LDAP Search Filters You Actually Need

By Prabath Thalangama· October 24, 2026· 4 min read
#ldap#active-directory#authentication

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 sub from the directory root to confirm the entry, then tighten.
  • (memberOf=...) misses nested groups — plain memberOf is 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 error on 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 (Z suffix); AD FILETIME attributes are a different format entirely.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.