Skip to content

Start typing to search

Active Directory Guide: Architecture, DNS, Security

What Is LDAP? Directory Queries, Bind, and LDAPS

What LDAP is, how a query is built from base DN, scope and filter, the three bind types, and why 389 vs 636 isn't a preference, shown with captured packets.

Written by

LDAP (Lightweight Directory Access Protocol) is the network protocol applications use to query and update the user, group and device records held in a directory. In an organization, the answer to “who is this user, what groups are they in, which unit do they work in” is kept in one central place, and applications ask that place with this protocol. It uses port 389 for a plain connection and port 636 for an encrypted one.

Advertisement

Picture a library. Hundreds of thousands of books on the shelves, and none of them sit at random: first the floor, then the section, then the shelf, then the position on that shelf.

The card in the catalog doesn’t give you the book’s name. It describes exactly where it sits. Knowing the name isn’t enough to find the book; knowing the location is.

LDAP works exactly like this, and we’ll stay in that library for the whole article.

Below we’ll first cover what LDAP is and what the “lightweight” in its name is lightweight compared to. Then we’ll build the directory’s shelving order, the DIT and DN concepts; then clear up the Active Directory versus LDAP mix-up that trips most people. After that we’ll look at how a query is built and how you prove your identity to the directory. Finally we’ll show why the difference between 389 and 636 isn’t a matter of preference, not by asserting it but with packets captured in our own lab: in the same bind, one carries the password across the network in readable form, the other leaves no trace. If you have a directory query that isn’t answering right now, you can jump straight to the diagnosis section. This article is part of the Active Directory guide.

What Is LDAP? A Directory Protocol, and the “Lightweight” in the Name

LDAP is an open protocol running between a client and a server, used to reach a directory service. Its current version is LDAPv3, and its specification is written in the document numbered RFC 4511.

RFCs are the internet’s rule book. They are numbered, publicly available technical documents; which message a protocol sends and how, what each field means, is defined there. The field names you’ll see inside the packets later in this article, such as name and authentication, aren’t invented, they come straight from that document.

Here’s why it matters: Windows, OpenLDAP, and the software inside a network printer were all written against the same document. They understand each other not because of a shared vendor but because of a shared text. In other words, LDAP is not a product, it is an agreement.

The key word here is directory. A directory is not a database, and that distinction explains the protocol’s entire design:

  • A directory is read often, written rarely. A user record changes a few times a year but is queried thousands of times a day. LDAP is optimized for that ratio.
  • A directory is hierarchical. It is made not of rows and columns but of nested branches.
  • A directory is distributed. Responsibility for a branch can be delegated to another server, and the client never notices.

The library has the same order. Catalog cards change rarely and are read constantly; the shelving is hierarchical; and responsibility for a section may be delegated to another floor.

What Was “Lightweight,” and Lightweight Compared to What?

The “lightweight” in the name doesn’t say LDAP is simple, it says it is lighter than what came before it. Before it there was a directory standard called X.500, and its access protocol was DAP (Directory Access Protocol). DAP was technically capable but needed the entire OSI protocol stack to run, which made it practically unusable on most university and corporate networks.

Developed in 1993 at the University of Michigan, LDAP kept the same directory model but ran directly over TCP/IP. The result was a protocol conceptually as capable as DAP but speaking the language the internet speaks. Today you’ll almost never see X.500 in the field; the concepts it left behind live on unchanged inside LDAP.

The practical consequence: most of the odd abbreviations you meet in LDAP are 1980s X.500 inheritance. Terms like dc, ou and cn look arbitrary at first; they aren’t, they are the remains of a library classification system.

LDAP Directory Structure: DIT, DN, and RDN

An LDAP directory is a tree, and that tree is called the DIT (Directory Information Tree). The organization’s root sits at the top, units below it, and individual users, groups and computers at the very bottom. Every object has exactly one place in the tree, and the expression that describes that place is called the DN (Distinguished Name).

The word “distinguished” isn’t there for nothing: a DN is what sets an object apart from everything else in the directory. No two objects can have the same DN, just as no two books can sit in the same position on the same shelf.

The library mapping fits exactly:

LDAPLibraryExample
DITAll of the shelvesThe whole collection in the building
dc (domain component)Building / collection namedc=ad,dc=sercebilisim,dc=com
ou (organizational unit)Floor and sectionou=Computers
cn (common name)The single book on the shelfcn=PCTEST01
DNThe book’s full location: book, shelf, section, buildingcn=PCTEST01,ou=...,dc=com
RDNThe book’s own label, without its shelfJust the cn=PCTEST01 part

A real computer record’s DN in our lab looks like this:

CN=PCTEST01,OU=Computers,OU=Head_Office,OU=Locations,DC=ad,DC=sercebilisim,DC=com

This line is not a name, it is an address. And it reads like an address, but in reverse:

  1. CN=PCTEST01 is the most specific part: the object we’re after itself. This is called the RDN (Relative Distinguished Name), its “name relative to the branch it sits in.”
  2. OU=Computers is the shelf that object sits on.
  3. OU=Head_Office is the section that shelf is in.
  4. OU=Locations is the floor that section is on.
  5. DC=ad,DC=sercebilisim,DC=com is the building itself, the domain.

There are three rules to watch, and all three cause field mistakes:

  • The order goes left to right, specific to general. The exact opposite of file paths. Anyone used to writing C:\Locations\Head_Office\Computers\PCTEST01 will build the DN backward on the first try.
  • The whole DN is unique, an RDN is unique only within its own branch. You can have a user with the same name in two different units; their DNs differ, so they don’t collide.
  • Spaces and non-ASCII characters cause headaches. That’s why the Head_Office name above is written with an underscore. If an OU name has a space, you have to quote the DN or use an escape character, and non-ASCII characters behave differently from client to client.

What Makes an Object an Object: objectClass and Attributes

Every record in the directory is an object, and every object has a type. The attribute that sets the type is objectClass: whether a record is a user, a group or a computer is what that field says. The type also determines which attributes the object can carry; that is called the schema.

The attributes you’ll meet often on user objects:

  • sAMAccountName: the short name a user types when signing in. Specific to Active Directory.
  • userPrincipalName: the modern sign-in name in the form user@ad.sercebilisim.com.
  • mail: the email address. The field applications most often use to match a user.
  • memberOf: the DN list of the groups the user belongs to. Most authorization decisions come out of this field.
  • distinguishedName: the object’s own DN.

The library equivalent: the catalog card also lists the author, the publication year and the genre, not just the location. The reason you can find a book without knowing its shelf is that those fields are filled in.

Active Directory Is Not LDAP: Active Directory Speaks LDAP

This is the most common misconception on the topic, and correcting it pays off directly during diagnosis. Active Directory is a directory service; LDAP is only one of the protocols used to reach that directory. They are not the same thing, and one can’t stand in for the other.

You see the difference most clearly like this: a domain controller speaks several protocols at once, and each one answers a different question.

ProtocolPortThe question it answers
LDAP389 / 636Which groups is this user in, which unit?
Kerberos88Is this person really who they claim to be?
DNS53Where do I find the domain controller?
SMB445Where do I read the group policy files from?

So LDAP tells you the authorization, it doesn’t prove the identity. The one that answers “this person is who they say” is Kerberos; LDAP’s job is the question “what is this person authorized for.” How authentication works and the logic of tickets is covered in the What Is Kerberos guide.

Why Do They All Come Together?

A library isn’t only its catalog. The catalog tells you which book is where, but if no one checks your identity at the door anyone walks in; if the building’s address is unknown no one can reach it; and if there’s no system carrying books to the shelves the catalog is an empty promise. A directory is the same: on its own, it is an incomplete answer.

An organization’s identity infrastructure has to answer four questions, and LDAP looks at only one of them:

  1. “Who is this person, what are they authorized for?” The directory’s job. LDAP is here.
  2. “Are they really who they say?” Authentication. A separate protocol is needed.
  3. “Where is the directory?” The client finding the server. Name resolution is needed.
  4. “How do the rules reach the machines?” Distribution of policy files. A share protocol is needed.

When Microsoft designed Active Directory in the late 1990s, it didn’t leave these four as separately installed products; it combined them into a single package and called that package a “domain.” Today, when you stand up a domain controller you don’t install Kerberos and DNS separately, they come on their own. This isn’t a convenience, it’s a requirement: AD can’t run without them, because it depends on DNS to make its own clients findable and on Kerberos to verify the user.

The conclusion here also explains this section’s heading. “Active Directory is not LDAP” isn’t wordplay: AD is a system that answers all four questions, while LDAP is only the language that system speaks when it answers the first one.

The Same Decision Was Made on the Linux Side

Seeing that this design isn’t specific to Microsoft is the best way to make the concept stick. In the Linux world, FreeIPA was born from exactly the same need and followed the same path: it contains 389 Directory Server for the directory, MIT Kerberos for authentication, BIND for name resolution and Dogtag for certificates. They all come in one install, managed from one place.

The comparison lands in this table:

OptionWhat it givesWhat it doesn’t
OpenLDAPDirectory onlyAuthentication, name resolution, policy distribution
FreeIPADirectory + Kerberos + DNS + certificatesWindows client management, group policy
Active DirectoryDirectory + Kerberos + DNS + share + group policyDeep native management of Linux clients

This table also answers the “which one should I install” question. If all you need is a user directory, OpenLDAP is enough and it’s light. But if you need to manage machines, set up single sign-on and distribute central policy, a directory alone won’t do the job; you’ll have to solve the other three questions yourself. That’s why organizations reach for the bundled packages.

What Does Knowing This Distinction Buy You in Diagnosis?

The four questions above aren’t an academic classification. The way they pay off in the field is very concrete: when a fault lands in front of you, you look not at the symptom but at which question went unanswered. If a user says “GPO isn’t applying,” that’s the fourth question, not a directory question; poking at the directory is wasted time. “I can’t sign in” is the second or third question. “The user is visible but has no permissions” is the first.

Take a common example: Group Policy won’t apply on one domain controller. Group Policy is not a directory question. The directory holds the definition of the policy, but the policy’s files live in the SYSVOL share and you reach them over SMB. So the place to look is the share layer, not the directory, and an application would show only “GPO isn’t applying” with neither “SYSVOL” nor “SMB” anywhere in the error. Every GPO lives in SYSVOL under a folder named with its own GUID; if some of those folders never replicated to one controller, the directory side says the policies exist while the share side has no files for them, and the client simply can’t apply them.

The lesson isn’t the rebuild method. Knowing which question a protocol answers is what moves you from guessing to measuring at the moment of a fault. Without it, the same fault can be hunted for days in directory settings, because the symptom looks like it comes from there. That’s also why we treat LDAP in such detail here: knowing a protocol matters not for installing it, but for knowing where not to look when it breaks.

The practical consequence: an application having an “LDAP integration” means it can connect to Active Directory, but says nothing about how it connects. The application might only be reading the directory, or it might be verifying the user’s password against the directory. The two have completely different security consequences, and you’ll see the difference in the bind section of this article.

The reverse is also true: Active Directory isn’t the only thing that speaks LDAP. OpenLDAP, 389 Directory Server, Apple Open Directory and the built-in directory of many network devices speak the same protocol. From a client’s point of view they all accept the same query; the difference is in the schema and the attribute names.

How Is an LDAP Query Built? Base DN, Scope, and Filter

An LDAP query is made of the answers to three questions, and without giving all three you won’t get a meaningful result from the directory:

  1. Where do I start? This is the base DN.
  2. How deep do I go? This is the scope.
  3. Which ones do I want? This is the filter.

The library has the same three questions: which section do you start from, do you look only at that shelf or the sub-shelves too, and which criterion do the books you want match.

Base DN: The Branch Where the Search Begins

The base DN is where the search takes root, and LDAP shows nothing that isn’t below that point. This sentence explains half the diagnosis section, so let me underline it: a wrong base DN doesn’t produce an error, it just produces an empty result.

To search the whole domain, the base DN is the domain root:

DC=ad,DC=sercebilisim,DC=com

To search only a specific unit, it’s that unit’s DN:

OU=Head_Office,OU=Locations,DC=ad,DC=sercebilisim,DC=com

Keeping the base DN narrow does two things: the query speeds up, and the set of users the application sees is limited. When connecting an application to the directory, setting the base DN to the domain root is the easiest route, but it gives that application the ability to see every object in the directory.

There’s a dependency here that’s often noticed too late: the precondition for being able to narrow like this is that the OU tree was designed to allow that narrowing. If users, computers and groups sit mixed together in the same branch, there’s no narrow base DN to point at; you’re forced to give the application the domain root. So how precise a directory query can be is decided not on the day you write the query, but on the day you build the OU structure.

Scope: How Deep Do We Go?

The search scope is one of three values, and the difference between them is the second most common answer to “why did the query come back empty”:

ScopeWhat it doesLibrary equivalent
baseLooks only at the base DN itselfRead only that one card
oneLooks only one level downOnly what’s on that shelf, no other shelves
subLooks at everything below the base DNThat section and every shelf below it
The nodes each of the three search scopes covers in the same directory tree. Base scope looks only at the base DN itself and sees none of the users below it, so the result is zero. One scope looks only one level down; it finds the three users there but doesn't enter the sub-OU, so the result is three. Sub scope looks at everything below the base DN, sees the user inside the sub-OU too, so the result is four. base one sub ou=Users ou=Users ou=Users 0 users 3 users 4 users only the base DN itself doesn't enter the sub-OU everything below it user sub-OU Same base DN, same directory. The only thing that changes is how deep it goes. The nodes each of the three search scopes covers in the same directory tree. Base scope looks only at the base DN itself and sees none of the users below it, so the result is zero. One scope looks only one level down; it finds the three users there but doesn't enter the sub-OU, so the result is three. Sub scope looks at everything below the base DN, sees the user inside the sub-OU too, so the result is four. base one sub ou=Users ou=Users ou=Users 0 users · only the base DN itself 3 users · doesn't enter the sub-OU 4 users · everything below it Same directory. The only thing that changes is how deep it goes.
The visual of the lab measurement: one doesn't see the user moved into the sub-OU and returns three results, sub returns four. The query itself and the base DN are the same in all three.

In practice what you want is usually sub, and it’s the default of most common clients. one is usually chosen not by accident but on purpose, for a good reason: to show an application only a certain layer of the directory. For example, if under ou=Users there’s also an ou=Leavers or ou=Service_Accounts branch, choosing one leaves those branches out of the query. By the same logic you can show a firewall only the main user layer for VPN authorization and hide the sub-branches.

And that’s exactly where the danger comes from: the setting is put in for the right reason, then the directory changes and no one remembers the setting. I produced this in the lab. I assumed there were three users under ou=Kullanicilar (the lab’s OU for users; the name is Turkish), then a branch opened and its users were moved into a sub-OU. Same base DN, same filter, the only difference is the search scope:

--- scope = one ---
cn: Ilker Pehlivan
cn: Aysegul Demir
cn: Mehmet Kaya
sonuc sayisi: 3

--- scope = sub ---
cn: Ilker Pehlivan
cn: Aysegul Demir
cn: Mehmet Kaya
cn: Zeynep Arslan
sonuc sayisi: 4

(The lab script prints its labels in Turkish; sonuc sayisi is the result count.) The branch user Zeynep Arslan doesn’t show up at all in the one query. What matters is that both queries completed successfully:

one  -> exit code 0
sub  -> exit code 0

So the server doesn’t even know anything is missing. It answered the question it was asked correctly: “who is one level below this branch?” Zeynep was two levels down, that’s all.

The field version is the scenario that makes this hard to diagnose: an application runs fine for months, then a new branch opens or users are reorganized by location, and only the new arrivals can’t get in. No setting was changed, no error is logged; the only thing that changed is the shape of the directory. This is one of the most common causes behind “the old staff can get in, the new hires can’t.”

Filter: Which Ones Match the Criterion

LDAP filters use a parenthesized syntax and look foreign at first. The rule is simple: each condition goes in its own parentheses, the operator goes at the front.

The patterns you’ll use most:

(objectClass=user)
(sAMAccountName=ilker)
(&(objectClass=user)(memberOf=CN=VPN_Users,OU=Groups,DC=ad,DC=sercebilisim,DC=com))
(|(sAMAccountName=ilker)(mail=ilker@sercebilisim.com))
(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))

What each does, in order:

  1. The first line brings all user objects. One condition, one set of parentheses.
  2. The second line finds the record whose sign-in name is ilker.
  3. The third line combines two conditions: the & sign means “and,” so it must be both a user and a member of the given group. Group membership is written with the DN, not the group name.
  4. The fourth line builds an “or” with the | sign: the record whose name is ilker or whose email matches.
  5. The fifth line filters out disabled accounts. The ! sign means “not”; the long string of numbers inside it is a special matching rule that tests a single bit in the userAccountControl field.

The fifth line looks intimidating at first but it’s the one that’ll help you most in the field. When you connect an application to the directory, the default filter usually pulls all users, and people who have left, whose accounts have been disabled, end up on that list too. This is the classic reason for departed-employee records still showing in an application.

LDAP Bind: How Do You Prove Your Identity to the Directory?

Bind is the opening step of an LDAP session: the client connects to the server and says “who I am.” The query only starts after that. The library equivalent is passing the reference desk before you sit down at the catalog terminal.

There are three kinds of bind, and the difference between them is the foundation of this whole article’s security section.

Anonymous Bind

The client presents no identity. It sees as much as the directory leaves open to everyone, and nothing more. It’s like the public catalog terminal in a library: you see how the shelves are arranged, you don’t see the borrowing records.

In modern Active Directory setups anonymous bind is largely closed. Only the top record where the server introduces itself, called the RootDSE, is readable anonymously; the rest of the directory needs an identity.

Simple Bind

The client sends a DN and a password. It’s as simple as the name says, and this article’s most critical sentence belongs here: in a simple bind, the password is not encrypted inside the protocol.

If there’s encryption it comes from the transport layer, meaning LDAPS or StartTLS must be in use. In a simple bind over plain 389, the password goes across the network in readable form. We’ll show this in the next section not as a claim but as a captured packet.

The library equivalent: showing your membership card while saying your password out loud in front of the counter. The transaction goes through, no one objects, but everyone in the queue heard it.

SASL Bind

SASL (Simple Authentication and Security Layer) is a framework that moves authentication outside the protocol. LDAP says “let this mechanism verify the identity” and hands the job off to it. In an Active Directory environment that mechanism is usually Kerberos, and the standard interface called GSSAPI makes the connection; you’ll see that name on the bind lines of Windows clients in Wireshark captures.

The advantage is clear: the password never goes on the network at any stage. This is the method Windows machines use by default when they connect to the directory, and it works without you doing anything. The trouble starts with third-party applications: many don’t support SASL and offer you only “server, username, password” fields. From that point you’re stuck with simple bind, and the only protection is at the transport layer.

LDAP, LDAPS, and StartTLS: Ports 389, 636, and 3268

There are three ports and two encryption methods; mixing the two up is the number one reason for a wrong firewall rule in the field.

PortWhatEncryption
389LDAPNone (can be added afterward with StartTLS)
636LDAPSConnection is inside TLS from the start
3268Global CatalogNone
3269Global Catalog over SSLYes

LDAPS and StartTLS do the same job in two different orders:

  • LDAPS (636) sets the connection up inside TLS from the start. The encrypted tunnel opens first, the LDAP conversation begins inside that tunnel. Its naming follows the same logic as HTTPS.
  • StartTLS (389) opens a plain connection first, then sends a “let’s switch to encryption now” command, and that same connection is upgraded to encrypted. It needs no separate port.

Both give the same security level and both require a certificate on the server. Where that certificate comes from is a separate decision: from your own internal certificate authority, from outside, or self-signed; when each of the three is right is covered in the AD Certificate Services guide. The practical difference: because LDAPS is a separate port, it needs a separate firewall rule, while StartTLS uses the existing 389 rule but requires the application to support StartTLS.

I measured in the lab that StartTLS is optional, and the result on its own explains why the warning in this section matters. I sent the same query to the same server, on the same 389 port, twice:

ldapsearch -x -ZZ -H ldap://127.0.0.1:389 ...   -> sonuc geldi (sifreli)
ldapsearch -x     -H ldap://127.0.0.1:389 ...   -> sonuc geldi (duz metin)

(sonuc geldi means a result came back; sifreli is encrypted, duz metin plain text.) The -ZZ parameter means “switch to StartTLS first, don’t connect if you can’t.” Both commands returned the same record; the only difference is that in one the traffic was encrypted. So even when the server supports encryption it doesn’t refuse the plain connection: the decision whether to ask for encryption is entirely on the client. A certificate being installed on the server does not mean the connections made to it are encrypted.

I Measured It: Open Port, Broken Service

I asked our own lab domain controller two questions, and the two answers contradicted each other:

QuestionAnswer
Are the ports open?389, 636, 3268 all three open
Does 636 actually set up an encrypted connection?No. It refused in all four TLS versions tried

Looking at the first row and saying “LDAPS works” is the easiest error. A port test only tells you the door is open, not whether anyone is sitting behind it.

The real answer is in the second row. The server closes the connection during the encryption negotiation, and it breaks at the exact same point in all four versions. That detail gives away the cause: if the problem were a version mismatch, at least one would behave differently. What’s missing is the certificate. The lab DC doesn’t have an internal certificate authority yet, and LDAPS doesn’t work without one.

The field version is a diagnosis trap, and no one did anything wrong:

  • The person writing the firewall opens the port, passes the test, considers the job done.
  • The person installing the application writes ldaps://, can’t connect, looks for the error in the application.
  • The layer in between, the certificate, is on neither of their checklists.

It always ends the same way: when ldaps:// doesn’t work, ldap:// is tried, that works, and the configuration stays that way. It’s abandoned not because encryption doesn’t work, but because no one investigated why it didn’t.

Global Catalog: Why Does 3268 Exist?

The Global Catalog is a special directory that holds a partial copy of the objects of all domains in a forest. Not every attribute of every object, but the most commonly searched attributes.

The library equivalent: each branch has its own full catalog, but there’s also a shared summary catalog of all branches. From it you learn at a glance which book is in which branch; for the book’s full detail you have to go to its own branch.

The practical result: in single-domain environments you won’t feel a difference between 3268 and 389. In a multi-domain forest, 389 sees only its own domain and users in other domains don’t show in the query. An application’s “I can’t see some users” complaint in multi-domain environments often comes from here.

The trap here is that 3268 is also unencrypted. In a multi-domain environment, pointing the application at the Global Catalog solves the user-visibility problem, but in doing so you’ve built the connection in plain text again. Its encrypted counterpart is 3269, and the same relationship as between 389 and 636 holds here: changing the port number alone doesn’t bring encryption, you have to choose the encrypted one.

Why Resistance to LDAPS Is a Serious Call

A common enterprise setup is automatic VPN authorization on the firewall using AD identities. The shape is this: the firewall’s LDAP Servers definition points at a domain controller; when a user wants to connect to the VPN, the firewall asks the directory; if the person is in the VPN_Users group, access opens. User management runs from one place, no separate VPN user list is kept. A clean design.

The load this setup carries is bigger than it looks. The firewall connects to the directory with a service account, and that account’s password sits in the device’s configuration. If the connection runs over 389, the firewall writes that password onto the network in the open on every query.

The mistake behind resisting LDAPS is treating a risk’s invisibility as the risk’s absence. In a man-in-the-middle attack the work in front of the attacker is surprisingly small:

  • They don’t need any permission in the directory. Someone with no account at all can read that traffic.
  • They don’t need to crack the password. It isn’t encrypted to be cracked; it’s going across as-is.
  • The only requirement is being on the same network segment. A switch port on the wrong VLAN, or an access point that’s slipped out of management, meets that condition.

And what’s captured isn’t an ordinary user password, it’s a service account that reads the directory with broad rights.

The recommendation is clear and not up for debate: every application that connects to the directory should use LDAPS or StartTLS. The cost of the move is a server certificate, and if an internal certificate authority is set up that cost is near zero. In return you get rid of a service account password wandering the network. In an enterprise network this isn’t a trade-off worth thinking about.

Encryption alone isn’t enough either, because the real matter is how privileged that account is. The service account of an application connecting to the directory is defined with more rights than it needs in most environments and stays that way for years; even if its password isn’t captured, that account is a risk in itself.

Watching LDAP Traffic at the Packet Level

This section doesn’t describe the protocol, it shows it. Above we said “in a simple bind the password isn’t encrypted”; now we’ll open the network version of that sentence packet by packet. Once you’ve seen it, you’ll never again see the difference between ldap:// and ldaps:// as a matter of preference.

All of the measurements were produced in our own lab, and the setup deliberately spanned two operating systems:

  • Directory server: openldap, OpenLDAP on Ubuntu 24.04, 192.168.1.25. Base DN dc=lab,dc=sercebilisim,dc=com.
  • Client: a Windows workstation, 192.168.1.115. It sent the queries with .NET’s System.DirectoryServices.Protocols library.
  • Capture: tcpdump on the server side, decoding with tshark 4.2.2.

This detail matters: the client is Windows, the server is Linux, and there’s no adaptation layer in between. The same query, in the same syntax, over the same port. That is the practical meaning of LDAP being a protocol; if it were a product’s feature, the two sides wouldn’t understand each other.

Plain Connection: What Shows on 389?

First the classic setup: the client connects to port 389, presents its identity with a simple bind, then searches for a user. The captured flow is below. Three things are enough to know before reading it:

  • Each line is an LDAP message. The number at the front is the frame number, i.e. the packet’s place in the capture.
  • 192.168.1.115 is the client, 192.168.1.25 is the directory server. The arrow direction shows who’s talking.
  • The name at the end is the message type. bindRequest, searchRequest and so on. These are the protocol’s fixed names.
  • ou=Kullanicilar is the lab directory’s OU for users; the name is Turkish and left as captured.
 4  192.168.1.115 → 192.168.1.25  bindRequest(1) "uid=ilker,ou=Kullanicilar,dc=lab,dc=sercebilisim,dc=com" simple
 6  192.168.1.25  → 192.168.1.115 bindResponse(1) success
 7  192.168.1.115 → 192.168.1.25  searchRequest(2) "dc=lab,dc=sercebilisim,dc=com" wholeSubtree
 8  192.168.1.25  → 192.168.1.115 searchResEntry(2) "uid=aysegul,ou=Kullanicilar,dc=lab,dc=sercebilisim,dc=com"
 9  192.168.1.25  → 192.168.1.115 searchResDone(2) success  [1 result]
11  192.168.1.115 → 192.168.1.25  unbindRequest(3)

The story of the six lines, in order:

  1. Frame 4: the client presents its identity. The simple word at the end tells the bind type, and it’s the key to the rest of this article.
  2. Frame 6: the server accepts. The bind succeeded, the session is open.
  3. Frame 7: the actual query. The base DN is the domain root, the scope is wholeSubtree, i.e. the sub we described at the start of the article.
  4. Frame 8: the single matching record comes back. The result itself is visible in the open too.
  5. Frame 9: the search is done, one result found.
  6. Frame 11: the client closes the session.

Everything is normal up to here. The problem starts when you look inside the fourth line.

The Password Sits in the Packet

When I pull the two fields inside the bindRequest message, this is what comes back:

uid=ilker,ou=Kullanicilar,dc=lab,dc=sercebilisim,dc=com    LabParola123

On the left is the bind DN, on the right the password itself. Not encrypted, not hashed, not even encoded. It reads the same way in the raw hex dump of the same packet:

0060  6c 61 72 2c 64 63 3d 6c 61 62 2c 64 63 3d 73 65   lar,dc=lab,dc=se
0070  72 63 65 62 69 6c 69 73 69 6d 2c 64 63 3d 63 6f   rcebilisim,dc=co
0080  6d 80 0c 4c 61 62 50 61 72 6f 6c 61 31 32 33      m..LabParola123

The right column is the readable version of the bytes on the left. The LabParola123 at the end of the last line is the exact bytes passing over the network cable. Someone who captures this packet doesn’t need to crack a password, run a dictionary attack, or spend any effort at all; reading is enough.

The library equivalent we set up at the start: saying your password out loud in front of the counter while showing your membership card. The difference is that on a network there’s no “front of the queue”; everyone listening on the same wire is the same distance away.

What Does the Windows Machine on the Same Network Do?

The most instructive part of this capture was something we didn’t plan. Because we didn’t keep the filter tight, another machine’s traffic mixed into the file too: a Windows server on the same network had a session open at the time, and the machine was sending a series of queries to the domain controller’s directory.

The interesting thing wasn’t what those queries were, it was that they couldn’t be read:

26  bindRequest(3)  "<ROOT>"  sasl
28  bindResponse(3) success
29  SASL GSS-API Integrity: searchRequest(4) "<ROOT>" baseObject
33  SASL GSS-API Integrity: searchRequest(6) "CN=NTDS Settings,..."
35  SASL GSS-API Integrity: searchRequest(7) "CN=Public Key Services,..."

Three things to read in these lines:

  1. bindRequest ... sasl: Windows presents its identity not with simple but with SASL. So it isn’t sending its password, it’s proving itself with a Kerberos ticket.
  2. SASL GSS-API Integrity: every query after the bind carries this label. Wireshark can see the message type but can’t open its content.
  3. The query itself is visible, its answer isn’t. Which object was asked stays readable, the data carried is protected.

So in a single capture file, on the same network, seconds apart, two different worlds stood side by side:

ConnectionBind typePassword on the networkQuery content
Windows machine → domain controllerSASLNoneProtected
Lab client → directory serversimplePlain textReadable

The conclusion, maybe this article’s most important sentence: the problem isn’t in the LDAP protocol, it’s in how the protocol is used. Same network, same protocol, same port number; one sends no password at all, the other writes it in plain text. Windows clients do this on the right side without you doing anything. What’s left on the wrong side are the third-party applications offered only “server, username, password” fields: firewalls, help desks, HR software, print servers.

StartTLS: Watching the Upgrade Happen on 389

The two captures above are the two endpoints: plain 389 where the password is in the open, and 636 where LDAPS is inside TLS from the start. Between them sits the thing the section above only described in words: StartTLS opens a plain connection on 389 and upgrades that same connection to TLS in place. That upgrade is worth seeing, not describing, so I captured it.

The setup needs only the directory server: I ran ldapsearch -x -ZZ against ldap01 and captured port 389. The -ZZ forces StartTLS. The capture was taken on the server itself, so both endpoints read 192.168.1.25. Here is the flow (frame number, protocol and info columns from tshark; TCP-only frames left out):

 4  LDAP     extendedReq(1) LDAP_START_TLS_OID
 6  LDAP     extendedResp(1)
 8  TLSv1.2  Client Hello
 9  TLSv1.3  Server Hello
10  TLSv1.3  Change Cipher Spec
...
12  TLSv1.3  Application Data
...

Read top to bottom, that’s the whole mechanism the previous section put in words:

  1. Frame 4: the very first LDAP message on the plain 389 connection is not a bind. It’s an extendedReq, the StartTLS request, and it goes across in the clear. The full field decode names it exactly:
requestName: 1.3.6.1.4.1.1466.20037 (LDAP_START_TLS_OID)
  1. Frame 6: the server answers extendedResp success, “go ahead.”
  2. Frame 8: a TLSv1.2 Client Hello on the same connection. From here the connection is being wrapped in TLS, and by frame 9 the two sides settle on TLSv1.3.
  3. Frame 12 onward: everything is Application Data, encrypted. The bind and the search now happen inside the tunnel, invisible, exactly as they did on 636.

The contrast is a second connection to the same port, without -ZZ. There the first LDAP message is the bind itself, in the open:

33  LDAP  bindRequest(1) "<ROOT>" simple
...
37  LDAP  searchRequest(2) "dc=lab,dc=sercebilisim,dc=com" baseObject
38  LDAP  searchResEntry(2) "dc=lab,dc=sercebilisim,dc=com"
39  LDAP  searchResDone(2) success  [1 result]

So the same 389 port carries the LDAP conversation two ways depending on one choice: send the StartTLS request first and everything after it is inside TLS, or skip it and bindRequest ... simple goes across in the clear the way LabParola123 did above. This is why the earlier warning holds: the port is the same, the server is the same, the certificate is the same; the encryption exists only if the client asks for it. On 636 there is no such choice, the connection is TLS from the first byte, which is why for a client you don’t control, 636 is the safer default.

Encrypted Connection: The Same Bind, Over 636

Now I change one thing: same client, same user, same password, same query. Only the port is 636 and the connection is LDAPS. The captured traffic:

 1  55204 → 636  [SYN]
 2  636 → 55204  [SYN, ACK]
 4  TLSv1    Client Hello
 6  TLSv1.3  Server Hello
 7  TLSv1.3  Change Cipher Spec
 9  TLSv1.3  Application Data
10  TLSv1.3  Application Data
12  TLSv1.3  Application Data

What stands out in this list is not what’s there but what isn’t. There isn’t a single bindRequest line. No searchRequest, no returned record, no username. After the TLS handshake everything left is Application Data, the encrypted payload that can’t be decoded.

Comparing the two captures by the same measures gives this table:

Measure389 (LDAP)636 (LDAPS)
Packets decoded as LDAP60
Is the bind DN visibleYesNo
Is the password in the raw dataYesNo
Are the query and returned record readableYesNo
TLS versionNoneTLS 1.3
EncryptionNoneAES-256

When I searched for the password in the raw data, it was found in the 389 capture and returned zero matches in the 636 capture.

The only difference is one port’s number and the presence of a certificate. The directory content, the user, the password, the query, are all the same. That’s the cost of encryption, against the hex dump above as the cost of not encrypting.

LDAP Diagnosis: Empty Results and Bind Errors

Diagnosing LDAP faults is hard, because the protocol’s two most common faults produce no error. A wrong base DN returns an empty result, a narrow scope returns an empty result; both are technically successful queries. The symptom on the application side is “users aren’t coming,” and there’s nothing in the log file.

So diagnosis starts from the symptom, not an error message.

SymptomLikely causeFirst to check
Query doesn’t error, result is emptyWrong base DN or narrow scopeSet base DN to the domain root and retry
Some users come, some don’tScope is one, users are in sub-OUsSet scope to sub
Departed employees show in the listFilter doesn’t exclude disabled accountsAdd a userAccountControl condition to the filter
Connection never formsPort closed or wrong server nameMeasure access to 389 / 636
636 open but TLS doesn’t set upNo valid certificate on the serverCheck the certificate store
Missing users in a multi-domain environmentQuery goes to 389Use 3268 (Global Catalog)

Reading Bind Error Codes

First let’s be clear about where these codes are read from, because it’s often confused: they are not records kept in the Windows event log (Event Viewer). They come inside the server’s answer to the bind request. So the thing you see in these three places rests on the same source:

  • The error message on screen when you click Test connection in an application’s directory settings.
  • The inside of the bindResponse packet in a Wireshark capture.
  • The line the application writes to its own log file (if it passes the server’s message through as-is).

When a bind fails, the returned code tells you whether the problem is in the identity or the authorization. Active Directory adds its own sub-code next to these, and the real information is there:

CodeMeaningThe real cause
49Invalid credentialsCredentials wrong, check the sub-code
49 + data 52eInvalid credentialsWrong password or the user doesn’t exist
49 + data 532Password expiredNo non-expiring password policy on the service account
49 + data 533Account disabledService account turned off
49 + data 701Account expiredAn end date is defined on the account
49 + data 775Account lockedWrong password attempts locked the account
32No such objectBase DN isn’t in the directory
50Insufficient access rightsBind succeeded but no right to read that branch

The second row is the result we measured in the lab for this article, and it corrects a widely held belief. In many tables online, data 525 is listed as “user not found” and data 52e as “wrong password,” separately, and you’re told that distinction helps in diagnosis.

I tested this on our own domain controller. I made six different bind attempts: a non-existent DN, a non-existent ad\user name, a non-existent UPN, then a genuinely existing account written three different ways with a deliberately wrong password. The result was the same in all six:

1) Olmayan DN            => kod 49, data 52e
2) Var olan + yanlis     => kod 49, data 52e
3) Olmayan ad\user       => kod 49, data 52e
4) Olmayan UPN           => kod 49, data 52e
5) Var olan UPN          => kod 49, data 52e
6) Var olan tam DN       => kod 49, data 52e

(Olmayan means non-existent, Var olan existing, yanlis wrong password, kod code.) The raw message the server returned was byte-for-byte the same each time:

80090308: LdapErr: DSID-0C09055B, comment: AcceptSecurityContext error, data 52e, v65f4

So Active Directory doesn’t distinguish between “no such user” and “wrong password.” This isn’t a shortcoming, it’s deliberate security behavior: if it did distinguish, an attacker could detect valid usernames in the directory one by one just by looking at error codes, without knowing any password. This is called user enumeration, and it’s a real risk in environments where LDAP is exposed to the internet.

The library’s reference desk behaves the same way: saying “we have no such member” would mean you could learn who’s registered just by asking. Instead it gives a single answer, “you can’t sign in.” It doesn’t say which part didn’t match, because saying so would give away its own records.

The practical result for diagnosis: when you see 52e, don’t reflexively reset the password. That code only tells you “this pair didn’t sign in”; the bind DN itself may be mistyped, and no number of resets will change the result. The right move is to verify the DN independently: copy the object’s distinguishedName field from the console and compare it exactly.

The other sub-codes in the table (532, 533, 701, 775) are taken from Microsoft’s documentation and were not produced in this lab. Because they relate to the account’s state, they are distinguishable codes: when the password is correct, AD doesn’t hesitate to say the account is locked or disabled, because at that point you’ve already proven your identity.

Knowing where these codes come from is more useful than memorizing the table: the values in the data field aren’t LDAP-specific, they are the hexadecimal form of Windows’ general system error codes. So 52e means 0x52E, whose decimal is 1326, which is Windows’ ERROR_LOGON_FAILURE. By the same logic 533 = 1331 = ERROR_ACCOUNT_DISABLED. If you hit a code that isn’t in the table, convert the hex to decimal and search Microsoft’s system error code list to find its full name and meaning.

And there’s an asymmetry worth noting: code 50 is the case where the bind succeeded but the query was unauthorized. Identity correct, authorization insufficient. On the application side this usually looks like “connected but nothing comes,” and gets mistaken for a base DN problem.

The Diagnosis Order

Don’t break this order; each step is a precondition for the next, and examining an upper layer while a lower one is broken is wasted time.

  1. Is the port open? If the connection can’t form at all, there’s no point examining directory settings. Measure TCP access for 389, 636 and, if needed, 3268.
  2. Does the bind work? Before writing any query, just try to connect. If you get a bind error the problem is on the identity side, and looking at the base DN is premature.
  3. Is the base DN correct? Temporarily set the base DN to the domain root. If results come, the problem is in the base DN; if it’s still empty, it’s in the filter.
  4. What does the filter bring? Reduce the filter to its plainest form ((objectClass=user)) and see the result come. Then add the conditions back one at a time.
  5. Is encryption really set up? If the connection is defined with ldaps://, verify the TLS handshake completes; if the port is open but there’s no certificate, the connection drops silently.

The value of this order: up to the fourth step you haven’t changed anything in the directory, haven’t recreated any account, and you’ve measured which layer the fault is in. In LDAP faults the most expensive mistake is intervening before measuring; resetting a service account’s password or reinstalling the application usually doesn’t remove the cause, it just clears the symptom for a while.

Advertisement

Conclusion: If the Catalog Is Built Right, No One Notices It

What makes LDAP hard to grasp isn’t its complexity, it’s its silence. Built right, no one notices it exists; a user is added to a group, the application sees them, access opens. When it breaks, it often doesn’t even produce an error message: the query runs successfully and comes back empty.

The chain we built in this article was exactly to break that silence. From the directory’s shelving order to the DN, from the DN to the query’s three components, from there to the bind types and encryption. When you meet an LDAP fault, the question you ask should no longer be “why doesn’t LDAP work” but “at which step did it come back empty.” The second is measurable, the first isn’t.

The library’s catalog is the only thing that knows where the books are. Before rearranging the shelves you have to understand the catalog; and when searching the catalog, if you don’t know which section you started from, the book not being there and you looking in the wrong place give the same result.

Questions about LDAP

If you have an Active Directory environment, no. The moment a domain controller is set up, its LDAP interface opens too and starts accepting queries on port 389; there is no separate product to install. A standalone LDAP server such as OpenLDAP only comes up when you want a central directory in a non-Windows environment.
Technically no, it keeps working. But a simple bind over plain 389 carries the username and password across the network in readable form; anyone listening on the same wire gets both. When the account in question is a service account that reads a wide slice of the directory, that is one password standing in for access to the whole directory. The cost of moving to LDAPS is a server certificate; the cost of not moving is an identity system on the wire.
They reach the same encrypted result in two different orders. LDAPS (port 636) wraps the connection in TLS from the very start, the way HTTPS does. StartTLS uses plain port 389: it opens an ordinary connection, then sends an extended request to upgrade that same connection to TLS in place, so no separate port is needed. Both give the same security level and both require a certificate on the server. The catch is that StartTLS is optional, so the client has to actually ask for it, otherwise the connection stays in the clear.
Because the two most common LDAP faults don't produce an error. A wrong base DN and a too-narrow scope both return an empty result, and both are technically successful queries: the server answered the question it was asked, there just was nothing under that branch or at that depth. The symptom on the application side is 'no users' with nothing in the log. Diagnosis starts from the symptom, not an error message: widen the base DN to the domain root, then set the scope to sub, and see which one fills the result.
No. A port test only tells you the door is open, not that anyone is sitting behind it. A server with no valid certificate still accepts the TCP connection on 636 but refuses the TLS handshake, so the port looks open while LDAPS is effectively down. The open port and the working service are two separate things, and the only way to tell them apart is to measure the handshake, not the port.

This article is adapted from a guide the author first published in Turkish on sercebilisim.com: LDAP ve LDAPS Nedir? Dizin Sorgulama ve 636 Portu

Advertisement

Written by

İlker Pehlivan

Network and systems engineer, founder of Serçe Bilişim

I run the networks and servers that other people's work depends on. Before founding my own consultancy I administered the backbone network, firewalls and core systems of a large multi-site organisation with thousands of users. I write about the things that actually broke.