Table of Contents
So if I said the words “Lightweight Directory Access Protocol”, do you feel my pain? if you don’t, keep reading. Otherwise, you likely know exactly where this is going to be going.
And for the record: this is something that I’ve struggled with for about 8 months, and I think it’s getting close to the point where I really should just give up… but I won’t.
LDAP, or, yes, Lightweight Directory Access Protocol, is a standardized protocol for providing directory information services, one of the most common implementations, Microsoft Active Directory, is used to manage an entire windows domain. Group policies, user accounts, all that fun stuff that you might just want to stuff into one central server, is there, in directory services.
While LDAP can be used for more, 9 times out of 10, it’s going to be used for managing user accounts. User objects, or entries as they’re called, contain the required username, password hash, POSIX UID and GID, login shell, home directory… you get the point. A system with the proper LDAP daemon (and authentication set up, usually a PAM module with linux) can use an LDAP server to determine who has user accounts on a system. Many web apps, like Nextcloud, Airsonic, Emby (kinda), GitLab, you get the point, can also be set up to allow LDAP managed authentication instead or (or in addition to) their internal authentication database. So this sounds awesome, right? I have so many services and things that need logins, I could set up some LDAP in this network, and have one place for managing everything. Even better, if I set it up right, then I can enable or disable per-service access as a user-by-user level, meaning if someone is using my Emby server and wants access to my music collection, I just need to enable Airsonic access and they’re good!
OpenLDAP is the implementation available on linux, and it has a server daemon called
slapd, for the Standalone LDAP Daemon.
It’s a fitting name, since every time I have to change something on it, it really feels like the process was just an elaborate
d to the face.
By itself it’s a good LDAP implementation, there’s just one issue: The main way to configure LDAP is to modify the LDAP structure.
Instead of most servers, which might have a
.conf file buried somewhere in
dc=config (I’ll get to LDAP naming schemes later, trust me).
You either create, destroy, or modify LDAP objects within the
dc=config tree to change the server’s configuration.
This is done by providing the command, like
ldapmodify, with, one, valid credentials, and two, a valid LDIF (LDAP Data Interchange Format) file to send to the server… meaning itself.
First: This means you have to be familiar with LDIF format to change configs.
And second, this means that anyone who somehow gets the username and password for the admin account (oops!) has 100% unrestricted access to the configuration files.
Given how LDAP works… pretty much you’re resting on an SHA password hash as the only thing between
slapd and someone trying to ruin it.
dn: cn=John Smith,ou=Legal,dc=example,dc=com changetype: modify replace: employeeID employeeID: 1234 - replace: employeeNumber employeeNumber: 98722 - replace: extensionAttribute6 extensionAttribute6: JSmith98
This LDIF file modifies two attributes, the
employeeNumber of the object
This is how you configure
LDAP uses a hierarchical format, like a set of folders in a file system.
The full name of an LDAP object is it’s Distinguished Name, or DN.
Each comma-separated component in a DN is another level of their hierarchy.
And like DNS, the right-most component is the highest level.
In that example above,
dc=com is the highest level, which contains
dc=example, which contains
ou=Legal, which contains the object
Notice how they all start the same way?
This is the type.
dc is a Domain Component,
ou is an Organizational Unit,
cn is a Common Name… there’s a few.
A very common structure is to put all the user accounts, for, say, my domain,
This is just… too verbose, okay?
Even better, the default admin account that has full access is just
cn=admin in that.
Every LDAP server has an internal server schema, which dictates what DN components exist, valid
objectClasses and the attributes they have, and so on.
You can’t just add new data fields to an object willy-nilly, it has to belong to one of its
objectClasses, and you can’t just make up a new one of those either without modifying the schema.
And.. the schema is all based on OIDs, so good luck just arbitrarily extending it.
LDAP does have a search command for the database,
It’s also the command to read an entry.
Every LDAP-compatible system I’ve seen will generally do something like search for the username you entered, and check if any of the objects that came back match your password.
If so, you log in as that user.
Some even allow you to set the actual query filter passed to the LDAP server to restrict the result set.
An example filter looks like this:
(&(|(objectClass=person))(uid=1000)) (This is almost what Nextcloud uses by default, substituting the
uid value with something else instead of a fixed value)
Let’s.. make that more legible:
(& (| (objectClass=person) ) (uid=1000) )
This filter in it’s weird Lisp-like syntax, means to only return entries where they have an
person, and a
There is technically a one-item OR compare on that
objectClass, but it’s effectively nothing.
you, of course, can get stupid with it:
(& (objectClass=person) (| (givenName:caseExactMatch:=John) (mail:caseExactSubstringsMatch:=john*) ) )
Only match entries with an
person and either a
John that matches the same case, or their
john (case sensitive).
Why is this syntax a thing?!
Base DN and Bind DN
For reasons, the “login” command in LDAP is
This presents an authentication mechanism, like plain or SASL, and a DN + password to bind with.
For most applications, you need a bind DN, the DN that it will issue a
BIND for to authenticate to the server to have enough authority to query other user entries and their password hashes.
There’s also a “base DN”, which is the root of the object tree searches are issued at.
If you keep all your users in
ou=People,dc=example,dc=com, then that will be the base DN.
In theory I can make each application it’s own OU, and set each one with a different bind DN for it’s OU, and since LDAP supports alias objects (pointers to another object), I can just add and remove aliases from OUs to control access permissions, right?
derefAliases field for the
If dereferencing is allowed, and an alias object is found, instead of just returning the alias object, the server will follow the link, and return the real object instead.
Well for some reason everything sets this to never, meaning aliasing is 100% out the window…. WHY?!
Hard-Coded UID and GID
Yes, for shell access (which some services do require), which linux can handle, easily, you need to provide the UID and GID of the user in the LDAP entry, as well as their login shell and their home directory.
I really hope every server you have uses the same UID and GID mappings, the same location for all shells, and the same directory for all home folders or else this will get fun.
Create a user with UID 1002, but John in Accounting ran
useradd manually and generated a UID 1002 on a specific machine?
Oops, you’ll have a UID conflict that you can’t easily change on the LDAP side.
I hope that local user wasn’t important, it’s gotta go.
I hope you thought about LDAP from the beginning or else you might have to re-map a bunch of users and groups by hand, what fun!
Again, I’ve been going at this for the better part of a year, and… Every time I start working on something, I actually have to ask myself, is it actually worth it to configure everything?