Local dev environment with LDAP server for pain and suffering
At my previous workplace, we had a very specific DX problem to solve: we had a pool of shared VMs used for testing. When you wanted to test out your PR you could grab a VM from the pool and deploy your changes there. At some point, after a few QA engineers overwrote each other's VMs a few times, we decided to write something that would allow you to "lease" the VM. The first iteration of this was a simple Telegram bot. User experience was a bit lacking, but it got the job done.
But one day, frustrated by the clunkiness of the solution, my colleague and I decided that the time for a makeover had come. After a brainstorming session, we ended up with a short list of what we wanted from this system. The three main points for it were:
- It needs to be a website (no more clunky messaging app interfaces)
- It needs to integrate with the company AD setup for auth
- It needs to be written in Rust
Easy enough, we already have experience with all of these, so this should not be a problem. After a few evenings of hacking, the clunky Telegram bot was reborn as tachikoma.
More and more people started to use it, and after a while, we got a request to add the ability to assign VMs to a specific team. Our junky at the time auth solution didn't cut it anymore - we now had to grab information about the groups the users are a part of.
At the time dev setup for LDAP was borrowed from ldap3 crate examples. With how our auth was set up, you could log in with empty credentials, very nice for dev environment. But now we had to query the user from the LDAP server, and this example setup would not cut it anymore. As any reasonable dev would do, we just ran all requests against a real AD server on the company infra. This meant that you had to have a VPN enabled when you were doing local development, but such is life.
Fast forward half a year or so, and I have now changed companies (and country of residency) and don't have easy access to the AD server anymore. Now I had no choice but to add a test LDAP server setup to the project.
LDAP
At first, I thought I could just adjust the ldap
crate setup and I'd be golden. Not having worked with
LDAP servers before, I got very quickly overwhelmed. It was definitely way too complex for what I needed,
so, a few stackoverflow posts later, I was reading Arch docs of all places to see how to set up an slapd
from scratch.
It netted good progress: I had an initial config that slapd
would accept and start.
config.ldif
# The root config entry
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: .ldap/run/slapd.args
olcPidFile: .ldap/run/slapd.pid
# Schemas
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
include: file:///etc/openldap/schema/core.ldif
# The config database
dn: olcDatabase=config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: config
olcRootDN: cn=Manager,dc=example,dc=org
# The database for our entries
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
olcSuffix: dc=example,dc=org
olcRootDN: cn=Manager,dc=example,dc=org
olcRootPW: test
olcDbDirectory: .ldap/db
olcDbIndex: objectClass eq
olcDbIndex: uid pres,eq
olcDbIndex: mail pres,sub,eq
olcDbIndex: cn,sn pres,sub,eq
olcDbIndex: dc eq
# Additional schemas
# RFC1274: Cosine and Internet X.500 schema
include: file:///etc/openldap/schema/cosine.ldif
# RFC2307: An Approach for Using LDAP as a Network Information Service
# Check RFC2307bis for nested groups and an auxiliary posixGroup objectClass (way easier)
include: file:///etc/openldap/schema/nis.ldif
# RFC2798: Internet Organizational Person
include: file:///etc/openldap/schema/inetorgperson.ldif
A few notes for me from the past:
olcArgsFile
andolcPidFile
need to point to an existing directory. This looks like it was designed withsystemd
in mind, so if you are using it - you should point it tounit
srun
directory.olcDbDirectory
is a path to the directory where the main database is going to be stored, so be mindful about access rights.- Take note of what value you used for
olcDatabase
for entry withobjectClass: olcMdbConfig
. This will come into play with later configuration. - Don't forget to set up
olcRootDN
andolcRootPW
, this is your credential for admin access
Before applying this configuration, we need to create a base directory structure. In my case, it's
mkdir -p .ldap/db .ldap/config .ldap/run
Now to apply this configuration, run
slapadd -n 0 -F .ldap/config -l ldap/config.ldif
where -F
is a directory where server configuration is going to be stored and -l
is a path to the configuration file.
The next step is adding some entries, which is also very straightforward:
config.ldif
# example.org
dn: dc=example,dc=org
dc: example
o: Example Organization
objectClass: dcObject
objectClass: organization
# Manager, example.org
dn: cn=Manager,dc=example,dc=org
cn: Manager
description: LDAP administrator
objectClass: organizationalRole
objectClass: top
roleOccupant: dc=example,dc=org
# People, example.org
dn: ou=People,dc=example,dc=org
ou: People
objectClass: top
objectClass: organizationalUnit
# Groups, example.org
dn: ou=Group,dc=example,dc=org
ou: Group
objectClass: top
objectClass: organizationalUnit
To apply this, you first would need to start up the slapd
daemon:
slapd -h ldapi://ldapi -F .ldap/config
After a second, you can add the base entries with
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/init.ldif
Note the -D
and -w
arguments - this is your credentials you've set up when configuring the server.
Now we are ready to add a user and a group! This config defines a user and a group he is a part of.
user.ldif
dn: uid=johndoe,ou=People,dc=example,dc=org
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: johndoe
cn: John Doe
sn: Doe
givenName: John
title: Guinea Pig
telephoneNumber: +0 000 000 0000
mobile: +0 000 000 0000
postalAddress: AddressLine1$AddressLine2$AddressLine3
userPassword: test
labeledURI: https://archlinux.org/
loginShell: /bin/bash
uidNumber: 9999
gidNumber: 9999
homeDirectory: /home/johndoe/
mail: jdoe@example.org
description: This is an example user
dn: cn=joe,ou=Group,dc=example,dc=org
objectClass: groupOfNames
objectClass: top
cn: joe
member: uid=johndoe,ou=People,dc=example,dc=org
To apply it, run
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/user.ldif
At this point I'm feeling pretty good: I have a working LDAP server with a user and a group.
Time to test the setup! Oh... User is missing memberOf
property?
memberOf
Remember that we needed to grab the information about the groups the user is a member of?
This is done with memberOf
attribute on the user. But when I query a user in does not have
such an attribute, though he is definitely a member of a group, so what is going on?
As it turns out, there is a so-called "overlay" that back-populates users with memberOf
attributes. And we have to enable it somehow.
Either Google is now useless, or there are no sane examples on how to do it (I suspect both), but after a few hours of delirium, I ended up with these three config files. Don't ask me what they do, I won't be able to answer that, I just know that they work and won't steal your crypto (probably).
overlay.ldif
# enable memberOf overlay, so infomation about user groups
# can be queried from the user
dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModuleLoad: memberof
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
refint.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcmoduleload
olcmoduleload: refint
refint2.ldif
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
objectClass: top
olcOverlay: {1}refint
olcRefintAttribute: memberof member manager owner
If you apply these configs before you add the user - you would have a memberOf
attribute for a user, that is a part of a group with objectClass: groupOfNames
.
So the script to set it all up in one go looks like this:
#!/usr/bin/env bash
mkdir -p .ldap/db .ldap/config .ldap/run
slapadd -n 0 -F .ldap/config -l ldap/config.ldif
slapd -h ldapi://ldapi -F .ldap/config
sleep 1
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/init.ldif
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/overlay.ldif
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/refint.ldif
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/refint2.ldif
ldapadd -x -D 'cn=Manager,dc=example,dc=org' -w test -H ldapi://ldapi -f ldap/user.ldif
Morale of the story
There is none. LDAP is a spawn of satan. For how ubiquitous it is - it's baffling how hard it is to find a good example configuration. Half of the time, I was just hitting digital rocks together.
Props to Arch docs, it again proves to be the GOAT.