Building a Homelab, Part 2 - LDAP and Single Sign-On

December 4, 2023 | 20 min. read


This is the second update in a series on the homelab I'm building. If you haven't read part 0 or part 1, do that first!

One problem that has popped up kind of consistently with the homelab is a proliferation of user accounts across the different self-hosted services. Jellyfin has its own user accounts and auth, as does Calibre, as does qBittorrent, etc. It's especially a pain with other people who share my homelab, and don't have a snazzy password manager
I use 1Password, and recommend it wholeheartedly. like I do and need to come up with a new password and remember it for every service.

Luckily, identity and access management (IAM) is pretty thoroughly trod ground and there's tons of open-source protocols and software that solve this for us.

Enter LDAP

One such protocol is LDAP, the Lightweight Directory Access Protocol. Pretty much every piece of software (especially enterprise-y stuff) that has the concept of "user accounts" can interface with LDAP, including most of the software running in the homelab. I didn't have much LDAP experience before this project, outside of using Active Directory when I worked at the help desk of my university's IT department in college (even then, it was basically just the world's most complicated password resetting tool). Despite being decades-old technology, it's actually pretty cool!

Despite being spoken about colloquially as an authentication system, LDAP isn't primarily concerned with auth. The best way I can think of to describe it is as an object-oriented database protocol. The database - which the protocol calls a directory - stores, organizes, and retrieves arbitrary entries that represent things like like user accounts or computers on a network. To massively simplify things, every entry in LDAP is uniquely identified by a distinguished name (DN). Entries can also have arbitrary attributes: names, phone numbers, email addresses, physical location, etc. LDAP also uses objectClasses to describe the type of thing an entry describes and what attributes should be on the entry. For example, a person objectClass would have attributes like cn (common name, or first name), sn (surname), telephoneNumber, userPassword, etc.

One of the great things about LDAP is that it's super old and encrusted with legacy usecases and conventions That's very rarely a good thing about other technologies. . You really don't need to know much about how it works or what you're doing with schemas to set things up and be productive. For decades, schemas for stuff like "a user on the computer network" have been standardized to the point that you can just fill in the blanks for your name, email, and password for your account and you're off to the races.

The Odds are Good, but the Goods are Odd

Weirdly enough, when I searched for "open source ldap servers", the results were pretty thin. There was Active Directory, but it's not open source and it's way too heavy for my use cases Active Directory is actually a combination of a bunch of different stuff and includes LDAP, DNS, and a Kerberos implementation. I don't need DNS since I already have a BIND server, and I wouldn't even know what to do with Kerberos. . There were some other candidates like FreeIPA that were open source, but they were also pretty heavyweight and seemed to do a lot more than just LDAP.

The two runners-up were OpenLDAP and Kanidm. OpenLDAP was super tempting, since it's a plain LDAP implementation and has been a standard choice for years. Unfortunately, I had heard from other homelabbers that it's a pretty complex tool and might be a little much for a small lab with just a handful of users (like my own). Regarding Kanidm, the documentation kind of made my decision for me with "Kanidm cannot be mapped 100% to LDAP's objects" and "Many of the structures in Kanidm do not correlate closely to LDAP" Taken from this page. . I also just got weird vibes in general from the project. The landing page doesn't look like the homepage of an IAM product at all; it looks like it's a Rust library or tutorial site. Not to start a flamewar or anything (I love the language as much as the next guy), but this is something that irks me about a lot of Rust projects. As an end user, I couldn't care less what language it's written in as long as it's easy to configure and deploy (ideally, with a Docker container). One could say that a lack of memory safety bugs and deadlocks and all the other stuff that Rust helps mitigate may be a selling point, but again - tell me about that, not the programming language it's written in!

Eventually, the LDAP server that I ended up deciding on was lldap. Ironically, it's also written in Rust.

El LDAP

lldap easily lands in the S Tier of things I'm running in the lab. It's really lightweight, the Docker Compose file to run it is right at the top of the README, and it has copy+pastable configurations for pretty much any self-hosted software that interfaces with LDAP. It was such a snap to set up with Calibre/Jellyfin/etc., I really don't have much to say or mention that isn't "two thumbs up!" or "to do this, just copy+paste this snippet from the examples".

Marcus Authelius

LDAP is cool and all for self-hosting identity, but a good amount of software still doesn't directly interface with it. It's understandable - if you want to log into Calibre with LDAP, you have to give Calibre the credentials to log into your LDAP server with. That's fine and dandy if you're self-hosting Calibre, but that's a more troubling prospect if you want to log into something that's hosted by somebody else.

In comes OpenID Connect. It's an extension of the OAuth protocol, which should be familiar to anyone that's integrated with third-party APIs like The Artist Formerly Known As Twitter, Google Play, Apple ID, etc. A lot of software (most commonly fediverse software like Mastodon) uses it as a kind of decentralized identity platform. With OpenID connect, you self-host a server (an "OpenID provider", in the jargon of the protocol) that listens for OAuth requests. You tell your server "Okay, you're allowed respond to requests from Mastodon". You can then go to a Mastodon server, tell Mastodon the URL of your OIDC server, and it'll redirect you to that OIDC server URL. Your server will ask for your creds and double check that you want to give Mastodon all the information that they're requesting, and after authenticating you should be redirected back to Mastodon along with all of the identity information that was requested. Mastodon will create an account for you based on the email/profile picture/name/etc. that OIDC gave to it, but Mastodon will always delegate authentication to the OIDC server. Again, OIDC is based off of OAuth, so if If you're not familiar with the OAuth protocol, it's actually pretty easy to pick up in about a day or so of reading. One great writeup is this one. sounds familiar, that's why!

The main service that I use that interfaces with OIDC (and kind of the main reason that I wanted to self-host an OIDC provider) is Tailscale. I've written about it in a previous post, but Tailscale's default offering for personal accounts is pretty limited when you want to share your Tailnet with other users. Luckily, they have the option to authenticate via OIDC and add up to three other users to your Tailnet. All Tailscale needs is a WebFinger server that points to your OIDC provider, and you're good to go.

I originally wanted to reuse my Gitea instance since Gitea includes both OIDC and WebFinger functionality by default, but it turns out those are best hosted separately (I'll get to that later). I ended up writing my own WebFinger server (again, wrote about that in my previous post) and setting up Authelia as a dedicated OIDC provider. I also felt like using Gitea as the core of my authn/authz was a little weird; it felt like using a hammer for both nails and screws. On the other hand, Authelia is kind of dedicated to the whole "get creds, give access" job.

Authelia is also useful for general authentication/authorization purposes. I run Traefik as the reverse proxy to all the services in the homelab, and there exists an integration between Traefik/Authelia that allows Authelia to be used as an auth middleware for arbitrary routes. lldap, like pretty much anything else, has a copy+pastable configuration for integrating with Authelia. Within about twenty minutes or so, I had a fully-configured Single-Sign On portal for any arbitrary service in the homelab, backed by the user accounts I had previously created in LDAP. Very cool!

Why Host WebFinger Separately From OIDC?

This was totally superfluous and unnecessary, unless you're a stickler about aesthetics and nice domain names like me. See, when you sign up with a custom OIDC provider with Tailscale, the Tailnet's domain is that of the WebFinger server - not the OIDC server's URL.

For example, if I want to sign up to Tailscale with noah@janissary.xyz, Tailscale is going to visit https://janissary.xyz/.well-known/webfinger to get all the information it needs (primarily, the OIDC issuer URL). Authelia has a WebFinger server built in, but my instance is running at auth.janissary.xyz. If I wanted to just use Authelia's builtin WebFinger instance, that means that I would need to sign up to Tailscale as noah@auth.janissary.xyz. This isn't really what I want, for a few reasons:

  1. My LDAP account in the homelab is noah and my homelab/VPS resides at janissary.xyz. My user account represents my presence across the entire homelab, not just my user on the authentication server. Therefore, semantically, I should sign up with noah@janissary.xyz.
  2. If I want to self-host email or run it under the same domain as this blog, I'd want my email address to be noah@janissary.xyz, not noah@auth.janissary.xyz. If I signed up for Tailscale with Authelia as noah@auth.janissary.xyz, I could have some headaches in the future as a result of the mismatch.
  3. noah@janissary.xyz is more minimalist and pleasing to me than noah@auth.janissary.xyz. A silly reason, but a reason nonetheless.

Thankfully, ditching Authelia's builtin WebFinger server provided pretty much zero consequences down the line. It seems like Tailscale only interacts with WebFinger during the initial creation of the Tailnet, and just interacts with the OIDC server for any later authorization or registration on the Tailnet.

Conclusion

Looking back on this post, I feel like I don't have nearly enough stuff to show for all the effort I spent. I hate hosting stuff without knowing enough to fix it when it breaks, so a pretty big chunk of time was just spent reading about LDAP/OAuth/OIDC just in case things went sideways (which came in handy with Tailscale!). I also spent a bunch of time faffing around with Gitea to make it work the way I wanted I even ended up contributing a PR to the project, so that the WebFinger response includes a link to the built-in OIDC issuer URL. , but that ended up being wasted work.

Regardless, this was a pretty fun (and useful) project for the lab. Besides the utility of having centralized credentials for everything, it's really empowering to host my own auth and get serious, real-life software on the actual Internet to rely on it. It's pretty easy to roll your eyes when you hear the cryptopunks and FOSS fundamentalists rant about "owning your identity". Every now and then, though, you start to understand what they're talking about.