Keeping your sanity while designing OpenLDAP ACLs

Ingo Bente
7 min readNov 26, 2016

--

Basically any corporate IT infrastructure relies on some sort of directory service. Most of the time, this means Active Directory. Sometimes, if you are lucky like me, this means OpenLDAP. Since such a directory service stores confidential information (like user passwords), you better make sure that your ACLs are set correctly.

Recently, I was involved in redesigning an existing OpenLDAP directory service. I did not expect the ACL settings to be that big of a deal. But I was wrong. Let me share some of my experiences here. Maybe it is useful to others, too.

Disclaimer: I am definitely no OpenLDAP expert. So if you find anything that does not make sense or could be done better, I would appreciate your comments below.

First things first — a security policy

So before you start with a deep dive in the syntax and semantics of your OpenLDAP servers’ ACLs, you should have a pretty good idea of what you are actually trying to achieve. A set of concise statements about who is allowed to do what will do the trick. For the project I was involved in, the requirements were as follows:

  1. There is a technical admin user that has write access to everything.
  2. LDAP admins have write access to everything by using their personal user accounts.
  3. A user has write access to a certain set of his attributes, including the password. This is mandatory to support any reasonable kind of self service. However, a user should not be able to change all of his attributes. For example, a user should not be able to change hist email address (otherwise, your IT department might have a really bad day). Obviously, a user should not be able to read or change the password of other users.
  4. Members of User Help Desk and HR have access to manage user accounts by using their own, personal user accounts. This involves task like resetting passwords, deactivating existing accounts and provisioning of new accounts.
  5. There will be technical service accounts. Think of a Samba server that requires read permissions on some user attributes (like sambaNTPassword which sadly is still a thing). Such accounts should have read access to basically the entire LDAP tree, but should avoid to be given write access. You don’t want to misuse your technical admin account as a service account either.
  6. This one is a bit tricky. Depending on your business, you will need some sort of group membership management. In order to keep your LDAP admins from freaking out, you need to come up with a distributed approach. We did the following: For each group, there is a specific set of managers. Each manager has write access to his group, so that he can add new members and remove existing members, but not to other groups.
  7. Anonymous cannot read or write anything.

Et voilà, you got yourself a security policy.

OpenLDAP ACLs in a nutshell

I found Zytrax’s LDAP for rocket scientists to be the best single point of information about anything related to OpenLDAP. In the following, I will provide examples for cn=config LDAP only. This means, you will add olcAccess attributes to your cn=config entry (instead of adding statements to your slapd.conf file).

The gist of the syntax is as follows:

olcAccess: to <what> by <who> <accesslevel> <control>

The what clause specifies the object which is about to be accessed. The by who clause specifies the user that wants to have access (aka the subject). The access level specifies the permission (like read or write). You can chain multiple by who statements in the same ACL. The last bit is an optional parameter (called control). We will save its purpose for later. There are plenty of nitty gritty details to get wrong, and you can tremendously mess things up. But the gist as stated above is enough to get you started.

Implementing our security policy

Let’s deal with the technical admin user first.

{0} to * by dn="cn=admin,dc=your-company,dc=com" manage by * break

This gives the admin user manage permissions to all entries in the LDAP. I did not research what themanage permission allows compared to a write permission, but the docs say that this the most privileged permission there is.

Remember the control parameter mentioned above? Now it comes into play. The by * break ensures that we can add further ACLs to the mix, and that those additional ACLs are actually processed. Otherwise, processing would stop right after the first ACL that has a matching what clause. Since our ACL matches any entry in the LDAP (to *), it would be the only ACL that is processed. This is because there is an implicit by * stop clause at the end of each ACL. So without the break, other users would not even be able to authenticate against the LDAP. Why the control is actually called break is beyond my knowledge.

Now on to the LDAP admins.

{1} to *by set.expand="([uid=] + ([cn=ldap-admins,ou=groups,dc=your-company,dc=com])/memberUid + [,ou=users,dc=your-company,dc=com])/entryDN & user" writeby * break

This one looks a bit complicated. But really what it does is giving the group ldap-admins write permission on the whole LDAP. Again, the break is needed to be able to process further ACLs. The regex style specification is required since in the LDAP I wrote the ACL for uses posixGroup groups (instead of groupOfNames). If you want to know the difference, start reading here.

Next, let’s ensure that users can have a nice self service. I have split this up into two ACLs. One for the confidential password attributes, one for the non-confidential attributes (like phone number). Let’s start with the confidential one.

{2} to attrs=sambaPasswordHistory, sambaPwdLastSet,
userPassword, sambaNTPassword
by set.expand="([uid=] + ([cn=ldap-user-management,ou=groups,dc=your-company,dc=com])/memberUid + [,ou=users,dc=your-company,dc=com])/entryDN & user" writeby self writeby dn.children="ou=system,dc=your-company,dc=com" readby anonymous auth

Let’s break it down. The what clause now explicitly names the attributes I am interested in. All of them are related to the users’ password. The first by who statement gives write access to members of the group ldap-user-management. In this group, you will likely want to put people from HR and from your User Help Desk. The second statement allows each user to change his own password attributes. The third gives read access to all entries below ou=system,dc=your-company,dc=com. This is where we can place technical service accounts that need read access to confidential attributes (like Samba clients). The last statement allows users to actually authenticate themselves against the LDAP. Note that there is no break this time. This means, the ACL is the only one that is processed for the attributes that we have specified in the what clause.

The ACL for the non-confidential self service attributes is slightly different.

{3} to attrs=telephoneNumber,mobileby set.expand="([uid=] + ([cn=ldap-user-management,ou=groups,dc=your-company,dc=com])/memberUid + [,ou=users,dc=your-company,dc=com])/entryDN & user" writeby self writeby users read

The first and second by who clause are identical to the previous ACL. Since any other user in the LDAP should be able to read the other users phone numbers, we just need a third statement which simply grants read permissions to all authenticated users.

Next, we will deal with the distributed group management. We need two ACLs for that. One for the manager groups and one for the user groups. We use a naming pattern so that we can regex the ACLs. Manager groups have the prefix manager- and user groups have the prefix user-. The following ACL allows manager groups to update themselves.

{4} to dn.regex="^cn=manager-(.+),ou=groups,dc=your-company,dc=com$" by set.expand="([uid=] + ([cn=manager-$1,ou=groups,dc=your-company,dc=com])/memberUid + [,ou=users,dc=your-company,dc=com])/entryDN & user" writeby users read

The next ACL allow manager groups to update the group they are responsible for.

{5} to dn.regex="^cn=user-(.+),ou=groups,dc=your-company,dc=com$"by set.expand="([uid=] + ([cn=manager-$1,ou=groups,dc=your-company,dc=com])/memberUid + [,ou=users,dc=your-company,dc=com])/entryDN & user" writeby users read

The way we use this in practice is as follows. Think of a company that has 100 projects. The goal is to have a distributed group management for each of the projects. That is, employees that are working on project unicorn should have access to the information of project unicorn, but not to information of any other project. In order to get access to project unicorn, employees must be in the correct LDAP group, here user-project-unicorn. The managers for this group are all members of the group manager-project-unicorn. They are responsible to add and remove employees to the user group user-project-unicorn.

This setup allows you to have a reasonably fine grained access model across your company, while still ensuring that the ACLs are manageable in a distributed way, limiting the amount of manual labour for your LDAP admins. Also note that although you have to manually add the specific groups, you do not have to change your ACLs thanks to the regex magic.

The last ACL simply grants read access to any entry below the base DN.

{6} to dn.subtree="dc=your-company,dc=com" by users read

And we are done.

Common pitfalls

As I said in the beginning, there are multiple ways to really mess things up when changing your LDAP ACLs.

Sequence: First, keep in mind that sequence is really important. If you throw an ACL with a broad to clause at the beginning of your configuration , chances are that you will not get what you want. For example, moving the last ACL from the previous section (the one starting with {6}) to position {2} would prevent your users from changing their self service attributes (including their passwords). As a bonus, any user would be able to read the password hashes of any other user.

Clients: You better understand your clients. For example, when testing the ACLs with a Synology NAS, I figured out that AFP was working without giving the technical user read access to the users’ password attributes, while SMB requires read access to them. This is because when using AFP, the user is authenticated by doing a bind against the LDAP on behalf of the user (thus the Synology does not have to know the users’ password hash). However when using Samba, the Synology does the bind with its technical user, reads the Samba password attributes, and authenticates the user based on them. Meh.

Conclusion

This was actually a lot of fun. I did not expect the OpenLDAP ACL concept to be that complex. However, we were able to implement our security polices with a pretty small number of rules.

What really bothers me are the security nightmares that are introduced by some clients. For example Samba. I mean, the password hash is based on MD4. Right, not even MD5, let alone any salted password stretching goodness. It’s MD4! In times where data breaches are the new normal, this is just something that we should fix. If you have read this and know an approach to get rid of those nasty MD4 hashes (while still supporting SMB as protocol for Windows clients), please let me know.

That’s all I have for now. Thanks for reading all this stuff.

--

--