last update: November 3rd 2020
This is the first part of debugging logon problems. The second one, an explanation of aaad.debug log, may be found here.
Recently I had to debug LDAP authentication on Citrix ADC / NetScaler and I started digging deeper.
I wanted to know how LDAP authentication really works, so I did what I always do in a case like that: I started with a network trace.
Attention: in 2018, when doing the original blog, plain text LDAP, TCP 389, had been common practice. Things changed. We now use secure LDAP, TCP 636. This will (in long future) be the only way supported by Microsoft.
- How to enable LDAP signing in Windows Server
- Enabling LDAP over SSLThis is a trace done on my NetScaler.
LDAP on port 636 is fully encrypted, so it would not be possible to explain the flow od date, that’s why this blog refers to plain text LDAP.
Attention! Different to default, my NetScaler is load-balancing LDAP-Servers. Therefore all packets don’t origin from NetScaler IP (NSIP) but from subnet-IP (SNIP). Usually all authentication is done by BSD and therefore origins from NSIP. I recommend load balancing LDAP servers!
I did analysis on all of these packets.
- bindRequest(1) “LDAP@intern.norz.at” simple
- bindResponse(1) success
- searchRequest(2) “dc=…” wholeSubtree
- seachResEntry(2) “dc=…” | seachResEntry(2) “CN=…”
- bindRequest(3) “CN=…” simple
- bindResponse(3) success
1st Packet (266), bindRequest(1)
So this is a bind request for LDAP2@intern.norz.at (called Administrator Bind DB, a miss-leading word as this should not be an admin at all for security reasons). In this case it is a service user. It’s permissions would be read only to the whole Active Directory, a permission usually every domain user has. It does not even need the user right log on locally.
Minimum requirements for Administrator Bind DN would be following:
- read for the Base DN (and the tree below) and the user object itself.
- read attributes like UPN or sAMAccountName containing the user name or attributes used in filters
- read for groups (only if you need group extraction)
- read for attributes PwdLastSet, UserAccountControl, msDS-User-Account-Control-Computed if you need to know if passwords are expired (and trigger password change from Citrix ADC / NetScaler)
2nd packet, response from LDAP server, bindResponse(1)
3rd packet, searching for the user’s LDAP name,
As mentioned above, we have to locate the user object to get the LDAP name for this user. The Base DN is the top most location in AD, where this user may be found, and of course we will search the whole AD below this location. In my case I search all of a LDAP-directory called intern.norz.at. To restrict to an OU called Enterprise Users we would set Base DN to: ou=Enterprise Users, dc=intern, dc=norz, dc=at.
4th packet, user’s LDAP name and group membership, seachResEntry(2)
The first section is about active directory properties needed. But we are interested in the second section: That’s where user object is located!
First of all, we located a user with LDAP name CN=Johannes Norz, OU=Users, OU=Our’s, DC=intern, DC=norz, DC=at. This user is located in an OU called users which is located in an other OU called Our’s. Normal users would not be able to remember user names like that, that’s why we have to do this step.
We also see, this user is member of two groups: PublishedApps and FaNorz (we see the LDAP names of these groups). This groups will be available from Citrix ADC / NetScaler. This user is also member of Domain Users, but we don’t see this group due to AD internal reasons. That’s why it’s not available in Citrix ADC / NetScaler.
5th packet: Logging on, bindRequest(3)
The password is transferred in plain test!
Maybe you don’t like it, but that’s what it is. So you should never use security type TCP PLAINTEXT (TCP 389) for LDAP authentication! Rather use SSL (TCP 636) or TLS (TCP 398) instead! This will encrypt all communication and therefore protect passwords.