The Endless Struggle of Single Sign On

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!

….right?

No.

The Server

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 slapd 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 /etc, slapd has 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 ldapcreate or 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.

LDIF

Example:

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 employeeID and employeeNumber of the object cn=John Smith,ou=Legal,dc=example,dc=com. This is how you configure slapd!

LDAP Naming

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 cn-John Smith.

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, tdstoragebay.com, into ou=People,dc=tdstoragebay,dc=com.

This is just… too verbose, okay?

Even better, the default admin account that has full access is just cn=admin in that.

LDAP Schema

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 Searching

LDAP does have a search command for the database, SEARCH. 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.

Filtering

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 its weird Lisp-like syntax, means to only return entries where they have an objectClass of person, and a uid of 1000. 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 objectClass of person and either a givenName of John that matches the same case, or their mail attribute starts with john (case sensitive).

Why is this syntax a thing?!

Base DN and Bind DN

For reasons, the “login” command in LDAP is BIND. 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 its 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?

No, because…

derefAliases

The derefAliases field for the SEARCH command. 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?