Postfix LDAP Howto


LDAP Support in Postfix

Postfix can use an LDAP directory as a source for any of its lookups: aliases(5), virtual(5), canonical(5), etc. This allows you to keep information for your mail service in a replicated network database with fine-grained access controls. By not storing it locally on the mail server, the administrators can maintain it from anywhere, and the users can control whatever bits of it you think appropriate. You can have multiple mail servers using the same information, without the hassle and delay of having to copy it to each.

Topics covered in this document:

Building Postfix with LDAP support

These instructions assume that you build Postfix from source code as described in the INSTALL document. Some modification may be required if you build Postfix from a vendor-specific source package.

Note 1: Postfix no longer supports the LDAP version 1 interface.

Note 2: to use LDAP with Debian GNU/Linux's Postfix, all you need is to install the postfix-ldap package and you're done. There is no need to recompile Postfix.

You need to have LDAP libraries and include files installed somewhere on your system, and you need to configure the Postfix Makefiles accordingly.

For example, to build the OpenLDAP libraries for use with Postfix (i.e. LDAP client code only), you could use the following command:

% ./configure  --without-kerberos --without-cyrus-sasl --without-tls \
    --without-threads --disable-slapd --disable-slurpd \
    --disable-debug --disable-shared

If you're using the libraries from the UM distribution (http://www.umich.edu/~dirsvcs/ldap/ldap.html) or OpenLDAP (http://www.openldap.org), something like this in the top level of your Postfix source tree should work:

% make tidy
% make makefiles CCARGS="-I/usr/local/include -DHAS_LDAP" \
    AUXLIBS="-L/usr/local/lib -lldap -L/usr/local/lib -llber"

On Solaris 2.x you may have to specify run-time link information, otherwise ld.so will not find some of the shared libraries:

% make tidy
% make makefiles CCARGS="-I/usr/local/include -DHAS_LDAP" \
    AUXLIBS="-L/usr/local/lib -R/usr/local/lib -lldap \
            -L/usr/local/lib -R/usr/local/lib -llber"

The 'make tidy' command is needed only if you have previously built Postfix without LDAP support.

Instead of '/usr/local' specify the actual locations of your LDAP include files and libraries. Be sure to not mix LDAP include files and LDAP libraries of different versions!!

If your LDAP libraries were built with Kerberos support, you'll also need to include your Kerberos libraries in this line. Note that the KTH Kerberos IV libraries might conflict with Postfix's lib/libdns.a, which defines dns_lookup. If that happens, you'll probably want to link with LDAP libraries that lack Kerberos support just to build Postfix, as it doesn't support Kerberos binds to the LDAP server anyway. Sorry about the bother.

If you're using one of the Netscape LDAP SDKs, you'll need to change the AUXLIBS line to point to libldap10.so or libldapssl30.so or whatever you have, and you may need to use the appropriate linker option (e.g. '-R') so the executables can find it at runtime.

Configuring LDAP lookups

In order to use LDAP lookups, define an LDAP source as a table lookup in main.cf, for example:

alias_maps = hash:/etc/aliases, ldap:/etc/postfix/ldap-aliases.cf

The file /etc/postfix/ldap-aliases.cf can specify a great number of parameters, including parameters that enable LDAP SSL and STARTTLS. For a complete description, see the ldap_table(5) manual page.

Example: local(8) aliases

Here's a basic example for using LDAP to look up local(8) aliases. Assume that in main.cf, you have:

alias_maps = hash:/etc/aliases, ldap:/etc/postfix/ldap-aliases.cf

and in ldap:/etc/postfix/ldap-aliases.cf you have:

server_host = ldap.example.com
search_base = dc=example, dc=com

Upon receiving mail for a local address "ldapuser" that isn't found in the /etc/aliases database, Postfix will search the LDAP server listening at port 389 on ldap.example.com. It will bind anonymously, search for any directory entries whose mailacceptinggeneralid attribute is "ldapuser", read the "maildrop" attributes of those found, and build a list of their maildrops, which will be treated as RFC822 addresses to which the message will be delivered.

Example: virtual domains/addresses

If you want to keep information for virtual lookups in your directory, it's only a little more complicated. First, you need to make sure Postfix knows about the virtual domain. An easy way to do that is to add the domain to the mailacceptinggeneralid attribute of some entry in the directory. Next, you'll want to make sure all of your virtual recipient's mailacceptinggeneralid attributes are fully qualified with their virtual domains. Finally, if you want to designate a directory entry as the default user for a virtual domain, just give it an additional mailacceptinggeneralid (or the equivalent in your directory) of "@fake.dom". That's right, no user part. If you don't want a catchall user, omit this step and mail to unknown users in the domain will simply bounce.

In summary, you might have a catchall user for a virtual domain that looks like this:

     dn: cn=defaultrecipient, dc=fake, dc=dom
     objectclass: top
     objectclass: virtualaccount
     cn: defaultrecipient
     owner: uid=root, dc=someserver, dc=isp, dc=dom
1 -> mailacceptinggeneralid: fake.dom
2 -> mailacceptinggeneralid: @fake.dom
3 -> maildrop: realuser@real.dom         

1: Postfix knows fake.dom is a valid virtual domain when it looks for this and gets something (the maildrop) back.

2: This causes any mail for unknown users in fake.dom to go to this entry ...

3: ... and then to its maildrop.

Normal users might simply have one mailacceptinggeneralid and maildrop, e.g. "normaluser@fake.dom" and "normaluser@real.dom".

Example: expanding LDAP groups

LDAP is frequently used to store group member information. There are a number of ways of handling LDAP groups. We will show a few examples in order of increasing complexity, but owing to the number of independent variables, we can only present a tiny portion of the solution space. We show how to:

  1. query groups as lists of addresses;

  2. query groups as lists of user objects containing addresses;

  3. forward special lists unexpanded to a separate list server, for moderation or other processing;

  4. handle complex schemas by controlling expansion and by treating leaf nodes specially, using features that are new in Postfix 2.4.

The example LDAP entries and implied schema below show two group entries ("agroup" and "bgroup") and four user entries ("auser", "buser", "cuser" and "duser"). The group "agroup" has the users "auser" (1) and "buser" (2) as members via DN references in the multi-valued attribute "memberdn", and direct email addresses of two external users "auser@example.org" (3) and "buser@example.org" (4) stored in the multi-valued attribute "memberaddr". The same is true of "bgroup" and "cuser"/"duser" (6)/(7)/(8)/(9), but "bgroup" also has a "maildrop" attribute of "bgroup@mlm.example.com" (5):

     dn: cn=agroup, dc=example, dc=com
     objectclass: top
     objectclass: ldapgroup
     cn: agroup
     mail: agroup@example.com
1 -> memberdn: uid=auser, dc=example, dc=com
2 -> memberdn: uid=buser, dc=example, dc=com
3 -> memberaddr: auser@example.org
4 -> memberaddr: buser@example.org

     dn: cn=bgroup, dc=example, dc=com
     objectclass: top
     objectclass: ldapgroup
     cn: bgroup
     mail: bgroup@example.com
5 -> maildrop: bgroup@mlm.example.com
6 -> memberdn: uid=cuser, dc=example, dc=com
7 -> memberdn: uid=duser, dc=example, dc=com
8 -> memberaddr: cuser@example.org
9 -> memberaddr: duser@example.org

     dn: uid=auser, dc=example, dc=com
     objectclass: top
     objectclass: ldapuser
     uid: auser
10 -> mail: auser@example.com
11 -> maildrop: auser@mailhub.example.com

     dn: uid=buser, dc=example, dc=com
     objectclass: top
     objectclass: ldapuser
     uid: buser
12 -> mail: buser@example.com
13 -> maildrop: buser@mailhub.example.com

     dn: uid=cuser, dc=example, dc=com
     objectclass: top
     objectclass: ldapuser
     uid: cuser
14 -> mail: cuser@example.com

     dn: uid=duser, dc=example, dc=com
     objectclass: top
     objectclass: ldapuser
     uid: duser
15 -> mail: duser@example.com

Our first use case ignores the "memberdn" attributes, and assumes that groups hold only direct "memberaddr" strings as in (3), (4), (8) and (9). The goal is to map the group address to the list of constituent "memberaddr" values. This is simple, ignoring the various connection related settings (hosts, ports, bind settings, timeouts, ...) we have:

    simple.cf:
        ...
        search_base = dc=example, dc=com
        query_filter = mail=%s
        result_attribute = memberaddr
    $ postmap -q agroup@example.com ldap:simple.cf
    auser@example.org,buser@example.org

We search "dc=example, dc=com". The "mail" attribute is used in the query_filter to locate the right group, the "result_attribute" setting described in ldap_table(5) is used to specify that "memberaddr" values from the matching group are to be returned as a comma separated list. Always check tables using postmap(1) with the "-q" option, before deploying them into production use in main.cf.

Our second use case instead expands "memberdn" attributes (1), (2), (6) and (7), follows the DN references and returns the "maildrop" of the referenced user entries. Here we use the "special_result_attribute" setting from ldap_table(5) to designate the "memberdn" attribute as holding DNs of the desired member entries. The "result_attribute" setting selects which attributes are returned from the selected DNs. It is important to choose a result attribute that is not also present in the group object, because result attributes are collected from both the group and the member DNs. In this case we choose "maildrop" and assume for the moment that groups never have a "maildrop" (the "bgroup" "maildrop" attribute is for a different use case). The returned data for "auser" and "buser" is from items (11) and (13) in the example data.

    special.cf:
        ...
        search_base = dc=example, dc=com
        query_filter = mail=%s
        result_attribute = maildrop
        special_result_attribute = memberdn
    $ postmap -q agroup@example.com ldap:special.cf
    auser@mailhub.example.com,buser@mailhub.example.com

Note: if the desired member object result attribute is always also present in the group, you get surprising results: the expansion also returns the address of the group. This is a known limitation of Postfix releases prior to 2.4, and is addressed in the new with Postfix 2.4 "leaf_result_attribute" feature described in ldap_table(5).

Our third use case has some groups that are expanded immediately, and other groups that are forwarded to a dedicated mailing list manager host for delayed expansion. This uses two LDAP tables, one for users and forwarded groups and a second for groups that can be expanded immediately. It is assumed that groups that require forwarding are never nested members of groups that are directly expanded.

    no_expand.cf:
        ...
        search_base = dc=example, dc=com
        query_filter = mail=%s
        result_attribute = maildrop
    expand.cf
        ...
        search_base = dc=example, dc=com
        query_filter = mail=%s
        result_attribute = maildrop
        special_result_attribute = memberdn
    $ postmap -q auser@example.com ldap:no_expand.cf ldap:expand.cf
    auser@mailhub.example.com
    $ postmap -q agroup@example.com ldap:no_expand.cf ldap:expand.cf
    auser@mailhub.example.com,buser@mailhub.example.com
    $ postmap -q bgroup@example.com ldap:no_expand.cf ldap:expand.cf
    bgroup@mlm.example.com

Non-group objects and groups with delayed expansion (those that have a maildrop attribute) are rewritten to a single maildrop value. Groups that don't have a maildrop are expanded as the second use case. This admits a more elegant solution with Postfix 2.4 and later.

Our final use case is the same as the third, but this time uses new features in Postfix 2.4. We now are able to use just one LDAP table and no longer need to assume that forwarded groups are never nested inside expanded groups.

    fancy.cf:
        ...
        search_base = dc=example, dc=com
        query_filter = mail=%s
        result_attribute = memberaddr
        special_result_attribute = memberdn
        terminal_result_attribute = maildrop
        leaf_result_attribute = mail
    $ postmap -q auser@example.com ldap:fancy.cf
    auser@mailhub.example.com
    $ postmap -q cuser@example.com ldap:fancy.cf
    cuser@example.com
    $ postmap -q agroup@example.com ldap:fancy.cf
    auser@mailhub.example.com,buser@mailhub.example.com,auser@example.org,buser@example.org
    $ postmap -q bgroup@example.com ldap:fancy.cf
    bgroup@mlm.example.com

Above, delayed expansion is enabled via "terminal_result_attribute", which, if present, is used as the sole result and all other expansion is suppressed. Otherwise, the "leaf_result_attribute" is only returned for leaf objects that don't have a "special_result_attribute" (non-groups), while the "result_attribute" (direct member address of groups) is returned at every level of recursive expansion, not just the leaf nodes. This fancy example illustrates all the features of Postfix 2.4 group expansion.

Other uses of LDAP lookups

Other common uses for LDAP lookups include rewriting senders and recipients with Postfix's canonical lookups, for example in order to make mail leaving your site appear to be coming from "First.Last@example.com" instead of "userid@example.com".

Notes and things to think about

Feedback

If you have questions, send them to postfix-users@postfix.org. Please include relevant information about your Postfix setup: LDAP-related output from postconf, which LDAP libraries you built with, and which directory server you're using. If your question involves your directory contents, please include the applicable bits of some directory entries.

Credits

And of course Wietse.