Controlling Authorization with ACLs

We've looked at connection security and authentication. Now we are ready to look at the last aspect of security: authorization. What we are specifically interested in is controlling access to information in our directory tree. Who should be able to access a record? Under what conditions? And how much of that record should they be able to see? These are the sorts of questions that we will address in this section.

The primary way that OpenLDAP controls access to directory data is through Access Control Lists (ACLs). When the SLAPD server processes a request from a client, it evaluates whether the client has permissions to access the information it has requested. To do this evaluation SLAPD sequentially evaluates each of the ACLs in the configuration files, applying the appropriate rules to the incoming request.

ACLs were introduced in Chapter 2 in the section entitled ACLs. This section will develop the basic examples discussed there.

An ACL is just a fancy configuration directive (the access directive) for SLAPD. Like certain other directives, the access directive can be used multiple times. There are two different places in the SLAPD configuration where ACLs can be placed. Firstly, they can be placed in the global configuration outside of a database section (that is, near the top of the configuration file). Rules that are placed at this level will apply globally to all backends. In the next chapter we will look at the case where a single directory has multiple backends.

Secondly, ACLs may be placed within a backend section (somewhere beneath a database directive). In this case, the ACLs will only be used when handling requests for information within database. In Chapter 2, we put our ACLs within the backend section, and we did not create any global access directives.

How does all of this work out in practice? When are global rules used, and when are backend-specific rules used? If a backend has no specific ACLs, then the global rules will apply. If a backend does have ACLs, then the global rules will only be applied if none of the backend-specific rules apply. If the request is for a record which is not stored in any backend, such as the Root DSE or the cn=subschema entry, then only the global rules will be applied.

Within their context ACLs are evaluated top-down, from the first directive in the configuration file to the last. So, when backend-specific rules are tested, SLAPD begins testing with the first rule on the list and continues sequentially until either a stopping rule matches or SLAPD reaches the end of the list.

In Chapter 2 we put the ACLs directly in the slapd.conf configuration file. In this section we will put them in their own file and use the include directive in slapd.conf to direct SLAPD to load the ACL file. This will allow us to separate the potentially lengthy ACLs from the rest of the configuration file.

Let's take a quick look at the format of an ACL, and then we will move on to some examples which will help clarify the intricacies of the ACL method.

An access directive looks like this:

access to [resources]

by [who] [type of access granted] [control]

by [who] [type of access granted] [control]

An access directive can have one to phrase, and any number of by phrases. We will take a look at the access to phrase first, then the by phrase.

In the access to part, an ACL specifies what is to be restricted in the directory tree by this rule. In the given rule we used [resources] as a placeholder for this section. An ACL can restrict by DN, by attribute, by filter, or by a combination of these. We will first look at restricting by DN.

To restrict access to a particular DN, we would use something like this:

access to dn="uid=matt,ou=Users,dc=example,dc=com"
       by * none

The rule would restrict access to that specific DN. Any time a request is received that needs access to the DN uid=matt,ou=Users,dc=example,dc=com, SLAPD would evaluate this rule to determine whether that request is authorized to access this record.

Restricting access to a specific DN can be useful at times, but there are several other supported options to the DN access specifier that come in useful for more general rule-making.

It is possible to restrict access to subtrees of a DN, or even by DN patterns. For example, if we wanted to write a rule that restricted access to entries beneath the Users OU, we could use an access clause like this:

In this example the rule restricts access to the OU and any records subordinate to it. This is accomplished by using dn.subtree (or the synonym dn.sub). In our directory information tree there are a number of user records in the Users OU subtree. These records are children of the Users OU. The DN uid=matt,ou=Users,dc=example,dc=com, for example, is in the subtree, and an attempt to access the record would trigger this rule.

Along with dn.subtree, there are three other keywords for adding structural restrictions to the DN access specifier:

The dn clause accepts one other modifier that can be used to do sophisticated pattern matching: dn.regex. The dn.regex access specifier can process POSIX extended regular expressions. Here is an example of a simple regular expression in dn.regex:

This example would restrict access to any DN with the pattern uid=SOMETHING,ou=Users,dc=example,dc=com, where SOMETHING can be any string that is at least one character long and has no commas (,) in it. Regular expressions are a powerful tool for writing ACLs. We will discuss them more in the section Getting More from Regular Expressions after we look at the by phrase.

In addition to restricting access to records by DN, we can also restrict access to one or more attributes within records. This is done using the attrs access specifier.

In the examples we've seen, when we restricted access we were restricting access at a record level. The attrs restriction works at a finer-grained level: it restricts access to particular attributes within records.

For example, consider a case where we want to limit access to the homePhone attribute of all records in our directory information tree. This can be done with the following access phrase:

access to attrs=homePhone
       by * none

The attrs specifier takes a list of one or more attributes. In the given example, we just restricted access to the homePhone attribute. If we wanted to block access to homePostalAddress as well, we could modify the attrs list accordingly:

access to attrs=homePhone,homePostalAddress
       by * none

Let's say that we wanted to restrict access to all of the attributes in the organizationalPerson object class. One way of doing this would be to create one long list: attrs=title, x121Address, registeredAddress, destinationIndicator,.... But such a method would be time-consuming, difficult to read, and clumsy.

Instead, there is a convenient shorthand notation for this:

This notation should be used carefully, however. This code does not just restrict access to the attributes explicitly defined in organizationalPerson, but also all of the attributes already defined in the person object class. Why? Because the organizationalPerson object class is a subclass of person. Therefore, all of the attributes of person are attributes of organizationalPerson.

Sometimes it useful to restrict access to all attributes not required or allowed by a particular object class. For example, consider the case where the only attributes we want to restrict are those that are not specified in the organizationalPerson object class. We can do that by replacing the at sign (@) with an exclamation point (!):

This will restrict access to any attributes unless they are allowed or required by the organizationalPerson object class.

There are two special names that can be specified in the attributes list but that do not actually match an attribute. These two names are entry and children. So we have two cases:

These two key words are not particularly useful in cases where only an attrs specifier is used, but they can be much more useful when attrs and dn specifiers are used in conjunction.

Sometimes it is useful to restrict by the value of an attribute (rather than by an attribute name). For example, we may want to restrict access to any givenName attribute that has the value Matt. This sort of thing can be accomplished using the val (value) specifier:

Like the dn specifier, the val specifier has regex, subtree, base, one, exact, and children styles.

With val.regex you can use regular expressions for matching. We can modify the last example to restrict access to any givenName that starts with the letter M:

In cases where the attribute value is a DN (like the member attribute for a groupOfNames object), the regex, subtree, base, one, and children styles can be used to restrict access based on the DN in the attribute value.

We have looked at three different access specifiers: dn, attrs, and filter. And in the previous sections we have used each. Now we will combine them to create even more specific access rules.

The order of combination is as follows:

access to [dn] [filter] [attrs] [val]

The dn and filter specifiers come first, as they both deal with records as a whole. Then attrs (and val), which function at the attribute level, come next. Let's say that we want to restrict access to records in the Users OU just in the cases where the record has an employeeNumber attribute. To do this we can use a combination of a DN specifier and a filter:

access to dn.subtree="ou=Users,dc=example,dc=com"
    filter="(employeeNumber=*)"
       by * none

This ACL will only restrict access when the request is for records in the ou=Users,dc=example,dc=com subtree and the employeeNumber field exists and has some value.

In a similar fashion, we can limit access to attributes for records in a certain subtree. For example, consider the case where we want to restrict access to the description attribute, but only for records that are in the the System OU. We can do this by combining the DN and attribute specifiers:

access to dn.subtree="ou=System,dc=example,dc=com" 
    attrs=description
       by * none

By this rule, a client could access the record with DN uid=authenticate,ou=System,dc=example,dc=com, but it would not be able to access the description attribute of that record.

By carefully combining these access specifiers it is possible to articulate exact access restrictions. We will see some more in action as we continue on to the by phrase.

The by phrase contains three parts:

  • The who field indicates what entities are allowed to access the resource identified in the access phrase
  • The access field (type of access granted) indicates what can be done with the resource
  • The third optional part, which is usually left off, is the control field

To get the gist of this distinction, consider the by phrase that we have been working with in the previous sections: by * none. In this by phrase, the who field is * (an asterisk character), and the access field is none. The control field is omitted in this example.

The * is the universal wildcard. It matches any entity, including anonymous and all DNs. The none access type indicates that no permissions at all should be granted to the entity identified in the who specifier. In other words, by * none means that no access should be granted to anyone.

We will explore the who field in detail, but before getting to that, let's examine the access field.

There are six distinct privileges that a client can have, in regards to an entry or attribute. There is also a seventh privilege specifier that equates to the removal of all privileges:

  1. w: Writes access to a record or attribute.
  2. r: Reads access to a record or attribute.
  3. s: Searches access to a record or attribute.
  4. c: Accesses to run a comparison operation on a record or attribute.
  5. x: Accesses to perform a server-side authentication operation on a record or attribute.
  6. d: Accesses to information about whether or not a record or attribute exists ('d' stands for 'disclose').
  7. 0: Does not allow access to the record or attribute. This is equivalent to -wrscxd.

These seven privileges can be specified in a by clause. To set one or more of these access privileges, use the = (equals) sign.

For example, to allow the server to compare a record's givenName field to a givenName specified by a client, we could use the following ACL:

access to attrs=givenName 
       by * =c

This will allow any client to attempt a compare operation. But that is the only operation it will allow. By this rule, no one can read from or write to this attribute. How does this work out in practice? When we use the ldapsearch client to attempt to read the value of the givenName attribute, we do not get any information about the givenName:

$ ldapsearch -LLL -U matt "(uid=matt)" givenName
SASL/DIGEST-MD5 authentication started
Please enter your password: 
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=matt,ou=Users,dc=example,dc=com

The only thing the server returns for our query is the DN of the record that matches the filter. No givenName attribute is returned.

However, if we use the ldapcompare client, we can ask the server to tell us whether or not the DN has a givenName field with the value 'Matt':

$ ldapcompare -U matt uid=matt,ou=Users,dc=example,dc=com \ 
    "givenName: Matt"
SASL/DIGEST-MD5 authentication started
Please enter your password: 
SASL username: matt
SASL SSF: 128
SASL installing layers
TRUE

The ldapcompare client sends a DN and an attribute/value pair to the server, and asks the server to compare the supplied attribute value with the server's copy of the attribute value for the record with the given DN.

Here the ldapcompare client will request that the SLAPD server look up the record for uid=matt,ou=Users,dc=example,dc=com and check to see if the givenName attribute has the value 'Matt'. The server will answer TRUE, FALSE, or (if there is an error) UNDEFINED.

In this case, the server responded TRUE. This indicates that the server performed the comparison, and the values matched. The combination of the ldapsearch and ldapcompare examples should illustrate how the ACL worked: while the server-side compare operation is permitted, the client does not have access to read the attribute value.

Multiple access privileges can be granted in one by phrase. To modify in order to allow reading (r), comparing (c), and disclosing (d) on the givenName attribute, we can use the following ACL:

Now, both the ldapsearch and ldapcompare commands that we ran should succeed.

There are cases where permissions are inherited from other ACLs (we will look at some later). In such cases, we can selectively add or remove specific permissions by using + (plus sign) to add and (minus sign) to remove.

For example, if we know that all the users already have compare (c) and disclose (d) on all the attributes, but we want to add read privileges just for the givenName attribute, we can use the following ACL:

Likewise, if we needed to remove the compare operation just for the givenName attribute, we could use a by clause like by * -c.

The 0 access privilege removes all privileges. It cannot be used with the + or operators, it can only be used with the = operator. The following ACL removes all privileges for all users to the givenName attribute:

This is the same as the by clause: by * -wrscdx.

These access controls are good for fine-grained control, but sometimes it is nice to have shortcuts. OpenLDAP has seven shortcuts that handle common configurations of access controls:

Keyword

Privileges

none

0

disclose

d

auth

xd

compare

cxd

search

scxd

read

rscxd

write

wrscxd

The none keyword we have seen before and it is the same as =0. Looking at the other keywords and their associated privilege, a pattern emerges: each keyword adds one new privilege, to the privileges of the previous keyword. Thus, auth has the =d privilege from disclose, plus the x privilege, and compare has =xd from auth and adds the c privilege. The write keyword at the bottom has all privileges.

Because this general accumulation of privileges captures the usual use cases while remaining more readable, keywords are used more frequently than privilege strings. In most of our examples from here on, we will use the keyword unless there is a specific reason to use the privilege string instead.

Now that we have covered the access field we will move on to the who field.

We have always used * in the who field. However, the who field is the richest of the ACL fields, providing twenty-three distinct forms, most of which can be used in combinations. In order to efficiently cover ground, we will cover the major forms on their own, and then group similar forms together and treat them as units.

The five most frequently used forms are *, anonymous, self, users, and dn.

The dn specifier performs similarly in the by phrase to the role it plays in the access to phrase. It specifies one or more DNs. The dn has the regex, base, one, subtree, and children modifiers, all of which perform the same way here as they did in the access to phrase. Here's an example using a few different DN patterns:

access to dn.subtree="ou=System,dc=example,dc=com" attrs=description
       by dn="uid=barbara,ou=Users,dc=example,dc=com" write
       by dn.children="ou=System,dc=example,dc=com" read
       by dn.regex="uid=[^,]+,ou=Users,dc=example,dc=com" read

This rule restricts access to the description attributes of anything in the System OU subtree. The user uid=barbara,ou=Users,dc=example,dc=com has write permissions to the description, while any child users of the System OU have read permissions. Users with DNs of the form uid=SOMETHING,ou=Users,dc=example,dc=com also have read access to the description.

In addition to the regular DN modifiers, a dn in the by clause can also have a level modifier. Level allows the ACL author to specify exactly how many levels down a by phrase should go. Recall that the dn.one specifier indicates that any record directly below the specified DN is to be granted the specified permissions. For example by dn.one="ou=Users,dc=example,dc=com" read grants any direct descendant of the Users OU read permissions. So uid=matt,ou=Users,dc=example,dc=com would be granted read access, but uid=jake,ou=Temp,ou=Users,dc=example,dc=com would not be granted such access because he is two levels down. The dn.level specifier lets us arbitrarily specify how many levels to descend. For example, by dn.level{2}="ou=Users,dc=example,dc=com" read would allow both matt and jake read access.

Sometimes it is useful to grant group members the access to an object. For example, if you have an Administrators group, you may wish to grant any member of that group write access to all of the records in the System OU.

One might expect that the way to set permissions for group members is simply to use the group as the value of a dn specifier in an ACL. However, that is not the case since the dn specifier refers to the group record as a whole, and has nothing at all to do with the members of the group, each of which has its own record elsewhere in the directory.

Instead, what we need is a way to search the member attributes of a particular group record, and then grant access to the DNs listed in the record. The group specifier provides exactly this sort of capability.

Group evaluation can be done with the group specifier. In its simplest form it is used like this:

access to dn.subtree="ou=System,dc=example,dc=com"
       by group="cn=Admins,ou=Groups,dc=example,dc=com" write
       by users read

This ACL will grant members of the cn=Admins,ou=Groups,dc=example,dc=com group write access to anything in the System OU, while giving all other users read-only permissions.

But the ACL above will only work on groups whose object class is groupOfNames, and whose membership attribute is member. This is because groupOfNames is the default grouping object class, and member is the default membership attribute.

When we created our LDAP Admins group in Chapter 3, it was not groupOfNames, nor did it use the member attribute for membership. Our record looked like this:

dn: cn=LDAP Admins,ou=Groups,dc=example,dc=com
cn: LDAP Admins
ou: Groups
description: Users who are LDAP administrators
uniqueMember: uid=barbara,ou=Users,dc=example,dc=com
uniqueMember: uid=matt,ou=Users,dc=example,dc=com
objectClass: groupOfUniqueNames

We used the groupOfUniqueNames object class and the uniqueMember membership attribute. In order to get the ACL to match these constraints we will need to specify the object class and membership attribute in the group specifier:

access to dn.subtree="ou=System,dc=example,dc=com"
       by group/groupOfUniqueNames/uniqueMember=
           "cn=LDAP Admins,ou=Groups,dc=example,dc=com" write
       by users read

Note the change in the highlighted line. Using slashes (/) we have specified first the object class then the membership attribute that should be used to determine who what entries represent members. When this by phrase is evaluated, SLAPD will find the DN cn=LDAP Admins,ou=Groups,dc=example,dc=com, check to see if it has object class groupOfUniqueMembers, and then grant write permissions to a DN if it is specified in a uniqueMember attribute.

Using this expanded notation, you can use other membership-based records as groups. For example, you can use the organizationalRole object class with the roleOccupant membership attribute.

Like many other specifiers, the group specifier also supports regular expressions with the regex style. Thus, we could create a rule that would allow members of any group in OU Groups write access to the System OU by expanding our last example:

access to dn.subtree="ou=System,dc=example,dc=com"
       by group/groupOfUniqueNames/uniqueMember.regex=
           "cn=[^,]+,ou=Groups,dc=example,dc=com" write
       by users read

The second and third lines should be combined into one long line in slapd.conf. The regular expression in the group specifier would match any DN with a CN component at the beginning. For all such entries, if the object class is groupOfUniqueMembers, then the SLAPD will grant membership to a user who is a uniqueMember of one of those groups.

SLAPD can use information about the client's connection (including network and security information) in access control lists. This feature provides an additional layer of network security that complements SSL/TLS and SASL.

The following are network or connection level specifiers:

  • peername: This is used to specify a range of IP addresses (for ldap:// and ldaps://).
  • sockname: This is used to specify a socket file for an LDAPI listener (ldapi://).
  • domain: This is used to specify a domain name for ldap:// and ldaps:// listeners.
  • sockurl: This is used to specify a socket file in URL format (ldapi://var/run/ldapi) for an LDAPI listener.
  • ssf: The overall security strength factor (SSF) of the connection.
  • transport_ssf: The SSF for the underlying transport layer of the network.
  • tls_ssf: The SSF for the SSL/TLS connection. This works with SSL/TLS connections on LDAPS listeners and Start TLS on LDAP listeners.
  • sasl_ssf: The SSF of the SASL connection.

The SSF specifiers (ssf, transport_ssf, tls_ssf, and sasl_ssf) perform the same checks as the SSF parameters to the SLAPD security directive (discussed in the first part of this chapter). In this case, however, SSFs may be used to selectively restrict (or grant) access to portions of the directory information tree. SSF specifiers require an integer value for the level of security desired. For example, using ssf=256 will require that the overall SSF of a connection be 256. But tls_ssf=56 will require that the SSF of the TLS/SSL layer be at least 56, regardless of what the SSF of the SASL configuration is. For more information on SSFs, see the section earlier in this chapter entitled Using Security Strength Factors.

For example, the following ACL will only grant write access to the specified DN when the client has connected with a strong SASL cipher:

access to dn.subtree="ou=users,dc=example,dc=com"
       by self sasl_ssf=128 write
       by users read

This rule allows users to modify their own records only if they have authenticated with SASL using a security mechanism with a strength of 128 (DIGEST-MD5) or more. All other users would only get read access.

The peername specifier is used for setting restrictions based on information about the IP connection. It can be used to complement other components in network security, like SSL/TLS. The peername specifier can take an IP address or a range of IP addresses (using subnet masks) and can also specify a source port.

The following rule grants write access to local connections, read access to connections on the local LAN (address from 10.40.0.0 through 10.40.0.255), and denies access to all other clients. Remember, every rule ends with an implicit by * none.

access to *
       by peername.ip=127.0.0.1 write
       by peername.ip=10.40.0.0%255.255.255.0 read

Note that the peername specifier requires the ip style for specifying an IP address. It also supports the regex style (access to * by peername.regex="^IP=10\.40\.0\.[0-9]+:[0-9]+$" write) and the path specifier to replicate the behavior of sockname.

A more useful version of the rule above would deny access to anything in the directory if it was not in the particular ranges, but would leave further access controls to rules appearing later in the ACL list. This can be done using the special break control described in the next section. We could also added SSF information, so connections coming over non-local connections must also use strong SSL/TLS encryption. Here is the rule:

The above rule might appear difficult to read, but here is what it does:

For more on the break control, see the section called The Control Field.

Sometimes it is more useful to be able to specify which domain names (rather than which IP addresses) should be granted access. This can be done with the domain specifier:

In the example above, the second line provides write access to any client connection coming from the domain name main.example.com. The third line grants read access to the domain example.com, and any subdomain of example.com. So, if a server with the domain name test2.example.com made a request, it would be granted access under the third rule. However, testexample.com would not match because it is not a subdomain of example.com—it is a different domain altogether.

When SLAPD encounters a domain specifier in an ACL, it takes the IP address of the client connection and does a reverse DNS lookup to get the host name. In light of this there are two things to keep in mind when using the domain specifier.

First, the name returned by a reverse DNS lookup may not be what you expect based on a forward DNS lookup. For example, doing a DNS lookup on ldap.example.com returns the address 10.40.0.23. However, doing a reverse DNS lookup on 10.40.0.23 returns mercury.example.com. Why?

It is because ldap.example.com is in DNS parlance, a CNAME record, and mercury.example.com is an A record. Practically speaking, what this means is that ldap.example.com is an alias to the server's real (canonical) name, which is mercury.example.com. The practical consequence is this: when you write an ACL using the domain specifier, make sure you use the A record domain name, not the CNAME record name. Otherwise, SLAPD will apply the rule to the wrong domain name.

The second thing to keep in mind when considering the domain specifier is that it is less reliable than using IP address information. DNS addresses can be spoofed, which means another server on the network can claim to be ldap.example.com and send traffic that looks, to SLAPD, like it is coming from the real ldap.example.com.

One way to diminish the risk of this is to use client-side SSL/TLS certificates and configure SLAPD to require that the client send a signed certificate to authenticate before it can perform any other directory operations. Unfortunately, client-side certificates cannot be selectively required through ACLs. Instead you will have to use the directive TLSVerifyClient demand in the slapd.conf file.

The sockname and sockurl specifiers are used for servers that run with UNIX local socket Inter Process Communication (IPC) instead of network sockets. These directives can be used to restrict local connections that use the IPC layer instead of connecting through the IP network.

For example, we could use the following ACL to allow only local (LDAPI) connections to write to the record, while users who connected through a different mechanism could only read the record:

The second line indicates that only LDAPI connections that connect through a particular LDAPI socket file should gain write access to the DN. All other clients (users) will get read permissions.

In addition to the syntax we have examined just now, there is an experimental type of by phrase—the set syntax. The set syntax can be used to create a compact and powerful set of conditions for access. Since it allows Boolean operators, and has a method for accessing attribute values, a single rule in the set syntax can accomplish what would otherwise take tremendously complex ACLs.

The basic idea behind the set syntax is this. By using a rule composed of conditions joined by operators, SLAPD creates a set of objects which have access to the record in question. If the result of an evaluation of a set specifier is a set that contains one or more members, then the by phrase is considered a match and permissions are applied. If, on the other hand, the set is empty, then SLAPD will continue evaluating the by phrases for that rule to see if it can find another match.

Here is a simple ACL using a set specifier to replicate the behavior of the group specifier. It provides write access to records in the System OU only to clients in the LDAP Admins group. All others get read access only:

The second line, highlighted above, contains the set specifier, which contains a set statement. The text in the square brackets specifies a DN, which is the DN of the LDAP Admins group. To access the values of the uniqueMember attribute we append /uniqueMember to the DN. When SLAPD expands this, it will contain the set of all uniqueMembers in the LDAP Admins group. In set-theoretic notation (which is not used by OpenLDAP, but which is helpful to understand what is happening), the set of group members would look like this:

There are two members (the two uniqueMembers) for the LDAP Admins group.

The & (ampersand) operator performs a union operation on two sets. The user keyword expands to the set that contains one member: the DN of the current client. So, if I perform a search, binding as uid=matt,ou=users,dc=example,dc=com, then the user set will contain one record:

{ uid=matt,ou=users,dc=example,dc=com }

When the & operator is applied, it will generate the intersection of the two sets. That is, the resulting set will contain only members that are in both of the original sets. Since only the record for UID matt is in both, the resulting set will contain just the DN for matt:

{ uid=matt,ou=users,dc=example,dc=com }

The resulting set is not empty so it is considered a match. The result of the set evaluation, then, is that the uid=matt,ou=users,dc=example,dc=com will be granted access based on the set specifier.

Consider a case though, when the user is not a member of the LDAP Admins group. If uid=david,ou=users,dc=example,dc=com binds, can he perform read and write operations? When the set specifier is run, the first of the two sets (group membership) will evaluate to the same thing it did above:

But the user keyword will expand to this:

There are no items in the intersection of these two sets, so the resulting set, after the & operator is applied, is an empty set:

There are no matches, so this by phrase fails to apply. The last line in our ACL (by users none) will then apply, and the uid=david will be given no access permissions.

Let's look at another example. We will use the set specifier to implement a rule where, when a client DN tries to access a record DN, it is given write access only if the two DNs are the same, or else it is given read access if they are in the same OU. Otherwise, the client DN is denied access to the record DN. Here's the ACL:

access to dn.subtree="dc=example,dc=com"
       by set="this & user" write
       by set="this/ou & user/ou" read

The first line indicates that this rule will apply to the record dc=example,dc=com and everything under it.

The second line takes the intersection of the sets generated by two keywords: this and user. The this keyword expands to the set containing the DN of the requested record. The user keyword, as we saw, expands to the DN of the client.

So, if the client uid=david,ou=users,dc=example,dc=com requests access to its own record, the resulting set operation will be as follows:

{ uid=david,ou=users,dc=exampls,dc=com } & 
    { uid=david,ou=users,dc=example,dc=com }

Since both sets contain the same member, the resulting set (the intersection of the two) is { uid=david,ou=users,dc=example,dc=com }. The end set is not empty, so the user will be granted write access.

Now let's look at the third line of the given ACL. This rule will return a non-empty set whenever the requested DN and the client DN both have the same value for the ou attribute. If uid=david,ou=users,dc=example,dc=com requests the record for uid=matt,ou=users,dc=example,dc=com, then SLAPD will check the values of their respective OU attributes.

The set identified by this/ou will be expanded to contain the values of all of the OU attributes in the requested record (the record for uid=matt,ou=users,dc=example,dc=com). This set is:

{ 'Users' }

Note that in this case the value is not a DN, but a string. Sets can perform matching operations on strings as well as DNs.

The set identified by user/ou will be expanded to contain the values of all of the OU attributes in the client's record. The record for uid=david,ou=users,dc=example,dc=com contains one value for the ou attribute, and the resulting set will contain that one attribute value:

SLAPD will compute the intersection of { 'Users' } & { 'Users' }, which is { 'Users' }. Since the set is not empty, uid=david,ou=users,dc=example,dc=com will be granted access to read the record of uid=matt,ou=users,dc=example,dc=com.

The set specifier provides one way of granting access to a record only in the case that a record contains a certain attribute. If we only want to grant write access to records with the title attribute, we can use the following rule:

In this ACL, if the requested record has a single title attribute, then the result of the evaluation of the above rule will be a set containing one element. However, if the record attribute has no title attribute, then the resulting set will be empty, and write access will not be granted.

In our directory the record of uid=matt,ou=users,dc=example,dc=com has the following title attribute:

But the record uid=barbara,ou=users,dc=example,dc=com does not have a title attribute at all. So if the record for uid=matt was requested, then the resulting set, based on the above ACL, would be:

So if an authenticated user attempted to access the record for uid=matt, SLAPD would grant access. In contrast, the set for uid=barbara would be {}, the empty set. So a user trying to access the record having uid=barbara would be denied access.

Using a similar set specifier, we could grant access to a record depending not only on the existence of an attribute, but on its value too:

According to the above rule, write access will be granted for anything in the Users OU only if the entry has an objectclass attribute with the value person. Note that in this case the square brackets are used to define a string literal.

If a client were to access the record uid=barbara,ou=users,dc=example,dc=com, the first part of our set statement would evaluate to the following set:

Those are the three object classes for the uid=barbara record. The other part, [person], would be expanded to this set:

When the union is computed, the result would be the set {'person'} and so write access would be granted.

These are just a few of the basic operations that can be done with the set specifier. Unfortunately, set is not documented in the slapd.access man page. However, there is a lengthy and informative article on using set in the OpenLDAP official FAQ-O-Matic: http://www.openldap.org/faq/data/cache/1133.html.

The last field in the by phrase is the control field. There are only three possible values for the control field: stop, break, and continue. If no control field is specified, stop is assumed. For example, by * none is the same as by * none stop.

The first value, stop, indicates that if that particular by clause matches, no further checking of ACLs for matching should occur. Consider the following (admittedly contrived) case:

access to attr=employeeNumber, employeeType, departmentNumber
       by users=cd
       by dn="uid=matt,ou=Users,dc=example,dc=com" +r

access to attr=employeeNumber
       by users +w

If I bind as uid=matt,ou=Users,dc=example,dc=com and try to modify my employeeNumber, will I be allowed to? No, I will not.

The reason I will not be able to modify the record is because I will only have the permissions granted by the first by phrase: by users =cd (remember, by users =cd is the same as by users=cd stop). As soon as SLAPD sees that I match the first by phrase of the first ACL, it will stop testing ACLs. Thus it will never reach the rule that grants my DN +r access, nor will it reach the rule that grants all users +w to the employeeNumber attribute.

This is an example of the stop control, which is used implicitly by all three rules.

Now, if I wanted to make sure that after the first by phrase SLAPD continues to evaluate phrases within the ACL, I could re-write the ACLs using the continue control:

access to attr=employeeNumber, employeeType, departmentNumber
       by users-=cd continue
       by dn="uid=matt,ou=Users,dc=example,dc=com" +r

access to attr=employeeNumber
       by users +w

After running the same test on these rules, the DN uid=matt,ou=Users,dc=example,dc=com would have the permissions =cdr.

The continue control instructs SLAPD to continue processing all of the by phrases in the current ACL. Once it is done evaluating that ACL though, it will not continue to look for matches in other ACLs.

In order to tell SLAPD to look at different rules for matches, we would have to use the break control. When SLAPD encounters an applicable clause that ends with a break control, it stops processing the current ACL but continues looking at other ACLs to see if they apply.

Thus, to get write permissions with our ACL we would want the following ACLs:

Now what will happen when the user with UID matt attempts accesses an employeeNumber?

First, the by phrase of the first ACL will be evaluated, and matt will be granted =cd. Because of the continue control, SLAPD will then examine the second by clause, which will also match for the user matt. Thus, matt will have =rcd when the processing of the first ACL completes.

Due to the break control the second ACL will also be evaluated, and matt will be granted +w as well, bringing his final permissions up to =wrcd.

Using the continue and break controls is one way to incrementally handle permissions. In complex configurations, judicious use of continue and break can make maintaining ACLs much easier, and can reduce the total number of ACLs.

In the previous sections we have looked at using regular expressions in both the access to phrase and the by phrase. But we can use both in conjunction. We can store information about the matches identified in the access to phrase, and use that information later in the by phrases.

To temporarily store matching information in an access to phrase we can surround the regular expression with parentheses. Here's an example:

access to dn.regex="ou=([^,]+),dc=example,dc=com"
       by dn.children,expand="ou=$1,dc=example,dc=com" read

This ACL grants a client the DN access to read a record DN only if both the client DN and the record DN are in the same part of the directory tree (that is, if both are in the same OU).

In the first line of the given ACL we used parentheses to capture the match from the regular expression [^,]+, which will be the value of the ou= component of the DN. Again, [^,]+ says "match all charcters that are not ','."

In the second line we used the dn.children specifier but supplemented it with an extra keyword: expand. The expand keyword tells SLAPD to substitute matches from the access to clause into this phrase.

Because of the expand keyword, the variable $1 is substituted with the value of the match in the first line. Everything captured between '(' and ')' in the regular expression will be stored in $1.

Variable names are assigned in order. The first set of parenthesis in the regular access to phrase gets stored in $1. If a second set of parenthesis existed, the matching information inside of those would be stored in $2 and so on for each additional set of parenthesis.

For example, we might want an ACL like this:

access to dn.regex="uid=([^,]+),ou=([^,]+),dc=example,dc=com"
       by dn.children,expand="uid=$1,ou=$2,dc=example,dc=com" write

This rule would grant a client DN access to read and write any entries subordinate to its own record but deny other uses the ability to even read those entries.

Notice that the first line stores two variables. The UID goes in $1 and the OU goes in $2. These are expanded in the second line.

It is also possible to use matches from the access to phrase in regular expressions in the by phrase:

access to dn.regex="uid=[^,]+,ou=([^,]+),dc=example,dc=com"
       by dn.regex="uid=[^,]+,ou=$1,dc=example,dc=com" write

In the first line only the results of the second regular expression are captured and stored in a variable. The second line also contains a regular expression, and it makes use of the $1 variable to retrieve the value of the OU from the first line. Note that dn.children,expand was replaced with dn.regex. The expand keyword need not be added for regular expressions.

The rule grants write access to a client DN for any user record that is in the same OU of that directory tree.

We have looked at some simple, though useful, regular expressions in these ACLs. But much more complex regular expressions can be composed, making ACLs even more powerful. As you compose more advanced regular expressions you may find some other sources of information helpful. Along with the slapd.access man page, the POSIX extended regular expressions man page (man regex) may turn out to be useful as well.

Debugging ACLs can be frustrating. They are complex, security sensitive, and require detailed testing. But there are three tools that make the debugging and testing process easier.

The first is just the ldapsearch command-line client. It can be used to carefully craft filters designed to test the processing of ACLs. The ldapcompare tool also comes in handy when you need to test comparison operations.

But it is also useful to make the most of LDAP's logging directives. The trace and acl debugging levels each provide detailed information about ACL processing. The acl level, for example, records each ACL evaluation. This can be very useful in determining what rules are run and when. We find the trace debugging level to be useful as well, as it provides information about how each evaluation was performed, including how regular expressions were expanded.

Finally, the slapacl command line utility provides a detail-oriented tool for evaluating ACLs directly. Since it does not connect to the SLAPD server over the LDAP protocol it allows more direct testing of just the ACLs.

For example, we can check whether or not a particular SASL user, matt, can access the record cn=LDAP Admins,ou=Groups,dc=example,dc=com and read the value of the description attribute:

The -U matt param specifies the SASL user name. The -b "cn=LDAP Admins,ou=Groups, dc=example,dc=com" param indicates which record we want to test against, and the last field, "description/read" indicates the attribute and the access level. This will simply return ALLOWED if the ACLs allow read access, or DENIED otherwise.

Likewise, we can test other LDAP operations. For example, we can test whether a user has permissions to compare:

In this example we have included the response. The first response line indicates how the SASL DN was resolved, and the second line indicates that compare access on uid was allowed.

The slapacl program essentially runs its own SLAPD and as such, it can be set to print complete processing logs to the screen. For example, to turn on trace debugging we can just add the -d trace param to the given command:

As you can seeslapacl provides detailed evaluation information in this case.

Using the LDAP command-line clients, detailed logging, and the slapacl command, debugging and testing ACLs can be done effectively.

In this part of the chapter, we have taken a low-level look at ACLs in OpenLDAP. We have covered many of the details of the ACL system. Now it is time to implement what we have covered so far to create a generic set of ACLs for our directory information tree.

In Chapter 2 we created a bare-bones set of ACLs in our slapd.conf file. Here's what we created then:

########
# ACLs #
########
access to attrs=userPassword
       by anonymous auth
       by self write
       by * none

access to *
       by self write
       by * none

Now, we will create a new, more practical list of ACLs.

The first thing we will do is move the ACLs out of slapd.conf and into a separate file: acl.conf. This will keep the lengthy list of ACLs separate from the rest of our configuration. To do this we will replace the ACLs above with an include directive:

########
# ACLs #
########
include /etc/ldap/acl.conf

When SLAPD is started it will include the contents of /etc/ldap/acl.conf at the location where the include statement appears. Recall that ACLs are backend-specific. Each different database can have its own ACLs (and multiple databases can be defined in the same slapd.conf file). So it is important to put the include directive after the database directive in slapd.conf.

Now we will begin editing the acl.conf file. The rules that we will write will be simple, and designed for a directory where most of the directory users are allowed to view most of the information in the directory. A higher-security directory may have a far more complex list of ACLs.

Since ACLs are evaluated in order from top to bottom we want to carefully craft our rules so that important restrictions are implemented right away.

If there are network-based access rules they should usually appear at the top of the ACL list so that they are evaluated first. For example, if we want to restrict access to the entire database if the host is not in our LAN, we would use the following rule:

By this rule only access from the localhost (127.0.0.1) and from inside of our 10.40.0.0 subnet will be allowed to access the directory. Since the break control is specified, later rules may modify the none permission, granting clients more permissions. All other connections will be closed immediately.

Next, we want to grant members of the LDAP Admins group write access to everything in the dc=example,dc=com tree:

This immediately grants write access to the members of the LDAP Admins group. For all other clients though, SLAPD will continue processing.

Next, we want to make sure that the userPassword field is available to the anonymous user for authentication purposes. We also want to allow users to be able to modify their own passwords, but otherwise we want userPassword unavailable for reading and writing by others. Note that by the previous rule the LDAP Admins will also be able to modify passwords for users.

In some cases, other users may need auth access to the password as well, in which case you may need to add by users auth to the given list.

We also need to grant access to the uid attribute if we are using the ldap:// URL form for SASL binding in the authz-regexp directive. This is because the filter in the LDAP URL is run as anonymous (see the discussion in the Configuring SLAPD for SASL Support Subsection).

Additionally, we don't want to let users try to modify their own uid, since uid is used in the DN:

Now Anonymous and all authenticated users will be able to access the uid attribute of any record in the directory to which they have access.

There are also a few other attributes we don't want users to be able to modify—even in their own records.

We don't want users to try to modify their OU attributes, since OU attributes are also used in DNs. We also don't want them to be able to modify their employeeNumber or their employeeType:

We have a special account, uid=Authenticate,ou=System,dc=example,dc=com, which will be used on occasion to help with bind requests. This user should not have access to anything else other than what we specified:

Again, the last line instructs SLAPD to continue processing ACLs for users who aren't having the authentication account. This line will also stop the anonymous user from browsing the rest of the tree since the implicit rule at the end, by * none, will catch the anonymous user.

Let's say that we don't want regular users (DNs in the Users OU) to be able to access records in the System OU of our directory (which is typically used for system accounts). We can implement this with the following rule:

This denies access to users in the Users OU, but allows other users (like System accounts) access to these records.

We also want to give every user the ability to read and write records below its own, but restrict others from accessing those records. This makes it possible for users to store their own information (like address books) inside of the directory:

Finally, the last rule we want is a default rule. This rule should answer the question, "What do we want to happen when no other rules are matched?" We want users to be able to modify their own records and see the records of others:

Now our list of ACLs is complete. Altogether, this is what they look like:

#################################################
# ACLs
# These are ACLs for the first database section
# of the slapd.conf file found in this directory
#################################################
##
## Restrict by IP address:
access to *
       by peername.ip=127.0.0.1 none break
       by peername.ip=10.40.0.0%255.255.255.0 none break

## Give Admins immediate write access:
access to dn.subtree="dc=example,dc=com"
       by group/groupOfUniqueNames/uniqueMember="cn=LDAP 
           Admins,ou=Groups,dc=example,dc=com" write
       by * none break

## Grant access to passwords for auth, but allow users to change 
## their own.
access to attrs=userPassword
       by anonymous auth
       by self write

## This rule is needed by authz-regexp
## (Note: Since uid is used in DN, user cannot change its own uid.)
access to attrs=uid
       by anonymous read
       by users read
## Don't let anyone modify OUs, employee num or employee type.
access to attrs=ou,employeeNumber,employeeType by users read

## Stop authentication account from reading anything else. This also 
## stops anonymous.
access to *
       by dn.exact="uid=Authenticate,ou=System,dc=example,dc=com" 
           none
       by users none break

## Prevent DNs in ou=Users from seeing system accounts
access to dn.subtree="ou=System,dc=example,dc=com"
       by dn.subtree="ou=Users,dc=example,dc=com" none
       by users read

## Allow user to add subentries beneath its own record.
access to dn.regex="^.*,uid=([^,]+),ou=Users,dc=example,dc=com$"
       by dn.exact,expand="uid=$1,ou=Users,dc=example,dc=com" write

## The default rule: Allow DNs to modify their own records. Give 
## read access to everyone else.
access to *
       by self write
       by users read

While they certainly won't meet all needs, these rules provide a good starting point for balancing security and usability of the directory. Furthermore, they set the stage for some of the things we will be doing later in this book.

In later chapters of this book, the mentioned ACLs will be revisited and fine-tuned to allow additional features, like directory replication.