As you can guess from the title of this mail, the LDAP support in DebOps has
been redesigned from the ground up. The changes can be seen in the PR, and
will probably be merged when this mail is sent out. This message is a more
in-depth look at the reasons behind the rewrite, lessons learned while I was
working on it, and the future of LDAP support in DebOps.
But first, some history
The 'debops.slapd' role, which manages OpenLDAP servers, was created pretty
early in DebOps history, in November 2014. At first, it was very basic
setup of the OpenLDAP service with horrible configuration of the administrator
passwords backed in. At the time I had no idea how LDAP really was supposed to
be used, and I just went by Debian documentation for OpenLDAP configuration
to set everything up. Around the same time, 'debops.auth' role and some others
gained preliminary LDAP support, so that hosts could use the LDAP directory as
the account database.
The OpenLDAP role was maintained over time with some small bugfixes. About
a year later, in November 2015 I started working on adding Mirror-Mode support
in the role, but I found out that the private keys generated by
'debops.pki' at the time weren't accepted by the OpenLDAP service because it
was compiled against GnuTLS, and keys were generated by OpenSSL. This prompted
me to radically redesign the 'debops.pki' role a few months later, which
spiraled into updating different DebOps roles for the new PKI support. The
work on 'debops.slapd' was shelved for a later date.
In the meantime, DebOps project was migrated from separate 'git' repositories
into a single monorepo, new functionality was implemented with custom filter
plugins that greatly simplified role design, documentation of the project was
improved, test-suite was redesigned... I found out some new role design
principles, 'ldap_attr' and 'ldap_entry' Ansible modules were merged into
After four years, work on OpenLDAP support could begin again.
The redesign begins
In the middle of February 2019 I started looking again into LDAP support in
DebOps, prompted by plans to implement it at my workplace. I knew at that
point that the redesign of 'debops.slapd' was inevitable - the current designs
of DebOps roles were too far removed from 2015 that reusing current role was
not feasible. At the same time, I wanted to experiment with creating
incremental changes in the codebase, recorded by small 'git' commits.
Previous redesigs of various roles were all done in 1 big commit, but that
didn't really show the process of redesigning the role and its various stages;
this time I wanted to show it properly.
I started by commenting out all of the role tasks so that the Ansible runs of
it were "empty" and began adding new elements one by one. At the same time,
I immersed myself in LDAP by reading various guides, blog posts and
documentation. The "LDAP for Rocket Scientists" book by Zytrax was very
useful for helping me understand how OpenLDAP, and LDAP directory itself works
and what can be done with it.
Very early on I decided to implement directory replication first, over
plaintext, to aid me with the role design. This was an important step that
helped me with changing the approach from configuring a single 'slapd'
instance at a time into setting up an entire OpenLDAP cluster from the ground
up. Unfortunately I quickly began to notice that old patterns from the
previous 'debops.slapd' role started emrging again - different parts of the
OpenLDAP configuration were put in more and more default variables. Somehing
had to give.
At the same time I started working on the 'debops.ldap' role, which was meant
to configure the support for LDAP "client-side", on other hosts in the
cluster. I looked at various uses of the 'ldap_entry' and 'ldap_attr'
Ansible modules in other DebOps roles, and tried to design a general solution
which could be used by other Ansible/DebOps roles to manage LDAP directory
entries. In the process, I created new roles for 'nslcd', 'nscd' and had
modifications to other DebOps roles which will be merged in the coming weeks,
after the main LDAP support is in the project. But the 'debops.ldap' role was
beginning to expand as well, and it was hard for me to come up with a light
design, so that it could be used in multiple playbooks for various services.
Fortunately at that point I found an Ansible proposal that deprecated the
'ldap_attr' module and replaced it with the 'ldap_attrs' module. In
essence, the PR modified the 'ldap_attr' so that it used the same mechanism of
YAML dictionaries for attribute configuration as 'ldap_entry', permitting
configuration of multiple attributes at the same time. This allowed me to
unify the configuration of the 'ldap_entry' and 'ldap_attrs' Ansible
into one list, and reduced the need for LDAP-related Ansible tasks to just
two, which then could be expanded in a loop based on the list of "LDAP tasks"
which the role had to perform against the LDAP directory.
This was exactly what I needed. Now, instead of expanding the 'debops.slapd'
role with more and more default variables, and corresponding hard-coded tasks,
I had to define only 1 YAML list for any LDAP tasks I needed. This also meant
that the 'debops.ldap' role could be expanded in the same way, with list of
tasks for the LDAP directory defined in a variable, instead of a set of
hard-coded Ansible tasks. This opened the path to defining LDAP tasks in
multiple roles which used 'debops.ldap' role as a dependency.
With the help of the custom DebOps filters that support intelligent merging of
configuration entries together, the new design for the 'debops.slapd' role was
complete, now what was left was the implementation. Because the 'ldap_attrs'
Ansible module is not included in Ansible core (I hope that with the new LDAP
support in DebOps it will get there), I included it in the
'debops.ansible_plugins' role. This way any Ansible role that use this one as
a dependency can have easy access to it.
Focusing on the default LDAP directory
After finalizing the main design elements of the 'debops.slapd' and
'debops.ldap' roles, I quickly updated them to the new format and started
focusing on the LDAP directory itself. At the same time I began work on the
new documentation for the two roles, so that I could update it along with the
main codebase of each role.
This time, adding TLS support in the OpenLDAP cluster was a success - the
OpenLDAP instances replicated the database between them without issues, and
LDAP clients finally began using StartTLS to secure connections to the
I started looking at support for POSIX environments in LDAP and
found out that the default Debian installation of OpenLDAP package used the
old RFC2307 schema to support posixGroup objects, meaning that this object
type couldn't be combined with other objects, for example groupOfNames. After
more research, I found out the updated rfc2307bis LDAP schema, and designed
a way to initialize the OpenLDAP database during Debian installation, so that
this new schema was used by default.
Then, more features started pouring in. In anticipation of a better Access
Control List management, I extended the existing 'ldap_attrs' Ansible module
with support for the 'X-ORDERED' OpenLDAP extension - this allows creation of
YAML lists of LDAP attributes which are automatically numbered in their list
order on the Ansible module level, before being applied in the LDAP directory.
This makes the modification of ACLs and other similar lists with ordered
values extremely easy and satisfying. I plan to suggest this change to the
original author of the module in the near future, having good examples of its
use might be an useful argument for its inclusion.
Extending the LDAP schema
After experimenting with design for a POSIX environment with User Private
Groups I concluded that the 'rfc2307bis' schema didn't go far enough with
its redesign of the 'posixGroup' object type - it lacked support for
POSIX-like group names. I tried to implement User Private Groups in different
ways using separate object tree, or subelements of the personal objects, but
in the end I concluded that the only reasonable way to implemet this would be
to create an additional attribute that extended the existing 'posixGroup'
That's how the DebOps project found itself in the IANA Private Enterprise
Numbers registry, with its own OID suffix, 53622. This allowed me to create
a new LDAP schema, 'posixgroupid', which is included in the
role and will be enabled by default on new installations. With it, User
Private Groups can be defined in the same LDAP objects as the user accounts
themselves, and POSIX groups can use separate, short names, rather than long
names with spaces that are based on the Distinguished Names of the group
Finishing the redesign
After this, updating the rest of the role was simple. What was left of the
redesign was expanding the documentation to include guides about Multi-Master
OpenLDAP replication, rewriting the backup script, and multiple days of
testing how the rest of the roles behaved with 'debops.slapd' and
'debops.ldap' to find and fix any issues with the new design based around the
LDAP tasks. Near the end of the redesign work I stumbled upon the
openldap-dit project and its Ubuntu Wiki entry which greatly helped me
in designing the default Access Control List included in the 'debops.slapd'
role, as well as inspired future directions on extending the OpenLDAP server
role with support for Kerberos, Samba, sudo and other services. But that's
planned for the future - first I wanted to prepare a solid base for LDAP in
DebOps which can be extended easily later on.
I finished with adding the 'debops.ldap' role in 1 commit, because
it was a new role in the DebOps project, and I hadn't really thought about
including it in smaller commits - an oversight I regret now, because half of
the redesign history was lost, again. I'll try to do it better next time.
The present and future of LDAP in DebOps
Right now, the 'debops.slapd' role sets up a basic OpenLDAP server with a few
extra LDAP schemas, a solid Access Control List based around groups of LDAP
accounts, and can quickly and easily deploy a clustered setup. The
'debops.ldap' role configures the system-wide LDAP client options, and can be
used to initialize a basic LDAP directory consistent with the default ACLs via
a custom playbook.
For the near future, I plan to focus on updating the LDAP support in the
existing DebOps roles to use the new infrastructure, and adding a few new
roles where necessary (nslcd, nscd, role for local admin accounts), along with
a new, separate bootstrap playbook that can set up a host to use the LDAP
directory from the start - no more need for local administrator accounts!
Well, they will be created just in case, but focus will be switched to the
LDAP directory if it's enabled.
The OpenLDAP server setup will also be expanded, with support for sudo,
Samba, Kerberos, along with some custom LDAP schemas like eduOrg and
eduPerson. I'm considering adding a role to setup FusionDirectory as
a frontend to the OpenLDAP cluster. I wasn't able to deploy it because of an
issue with the application code that didn't correctly detect the BaseDN of the
directory, so this still needs to be researched.
Application roles like 'debops.gitlab', 'debops.owncloud' and others
in DebOps will be updated to automatically create required objects in the LDAP
directory and use it as the source of account information. In the long run,
I suspect that LDAP directory will become a central data source for various
DebOps services - DNS zone database, DHCP/RADIUS authentication database, SSH
public key repository, and so on. The design of the 'debops.ldap' role allows
for this functionality to be optional and enabled on demand, therefore various
DebOps roles will be still usable without LDAP directory present. But in the
larger environments, replicated LDAP cluster setup will probably become one of
the first things to do, after configuring the Ansible Controller.
I'm glad that I found the time and will to focus on LDAP support and do it
properly this time. Sorry that it took so long and I hope that you will like
it as much as I do.
See you out there.