I’ve been running a Postfix+Dovecot with MySQL as the backend for eSource for some time now. It’s on old CentOS 5, but it really did wonders for our ability to manage Virtual email accounts. We’ve been using postfixadmin to manage the MySQL database. Recently, I’ve been getting into LDAP, and have found some success migrating to CentOS 6.

The goal here is to simply replace MySQL with LDAP. (This isn’t a “tutorial”, so I can’t guarantee that copying and pasting anything will work. I frequently copy something real, and then massage the text to eliminate sensitive data. I may or may not be consistent with my changes.)

First, I have to duplicate my vmail user that does all the email transactions.

useradd -u 101 vmail -g mail -s /sbin/nologin -d /var/vmail

Then I copied over my postfix config, which is already configured for Dovecot and for MySQL. If you are starting from scratch, I pity you, and I’m sorry I don’t have time to start postfix from scratch. (On the plus side, the dovecot stuff below is “from scratch”.) So I left all the dovecot stuff the same, and found all the mysql entries and changed them to ldap in /etc/postfix/main.cf:

virtual_mailbox_domains         = proxy:ldap:$config_directory/ldap_virtual_domains_maps.cf
virtual_mailbox_maps            = proxy:ldap:$config_directory/ldap_virtual_mailbox_maps.cf
virtual_alias_maps              = proxy:ldap:$config_directory/ldap_virtual_alias_maps.cf

I picked up a really great tip from Postfix’s VIRTUAL_README page. It suggests, “The reader is strongly advised to make the system work with local files before migrating to network databases, and to use the postmap command to verify that network database lookups produce the exact same results as local file lookup.” I’ll say it right now, “postmap -v” saved me HOURS of troubleshooting. The LDAP README article is an excellent starting position and explains some of the basic configuration settings. My setup turned out to not include some of the attributes that postfix defaults to, so I had to make some adjustments. First, the virtual_domains list is what postfix uses to determine if it should handle the mail locally, or lookup the domain’s MX record, and send it over the internet. I created an LDAP “Organisational Unit” and named it “Domains”, and put all my domains underneath, like so:

dn: ou=Domains,dc=example,dc=com
description: Domains is used for Postfix as it's list of locally hosted doma
objectclass: organizationalUnit
objectclass: top
ou: Domains

And then add a domain:

dn: dc=example.com,ou=Domains,dc=example,dc=com
dc: example.com
objectclass: dNSDomain
objectclass: top

In LDAP, there are two main parts of the schema, the ObjectClass and the Attribute. The LDAP database is a tree, so everything is a parent or a child of another entry. ObjectClasses define which attributes are available, and if they are required or not. You can only add attributes to an entry if the ObjectClass lists it.

Now, in ldap_virtual_domains_maps.cf, I filter the query to be just dNSDomain objectclasses that have a “dc” attribute equivalent to the domain being looked up. The attribute that carries the value I’m interested in is “dc”.

server_host = ldap://localhost/
search_base = ou=Domains,dc=example,dc=com
version = 3
bind = no
query_filter = (&(ObjectClass=dNSDomain)(dc=%s))
result_attribute = dc

If you are familiar with SQL, the postfix variable “result_attribute” is like your “SELECT” clause (but your limited to only one column), and the postfix variable “query_filter” is an LDAP query_filter and is like your WHERE clause. In LDAP, the query_filter syntax here is doing an AND logical operation on both conditions. I don’t have a good reference for the filter syntax, but google does return some good results for “ldap filter syntax”.

With out having to even bother restarting/reloading postfix, you can test the configuration file with postmap, like this:

postmap -v -q example.com ldap:/etc/postfix/ldap_virtual_domains_maps.cf

If you leave out the -v, you either get a result (and a return value of 0), or no result (and a return value of 1). With -v, you get to see all the variables that postfix uses to query the LDAP server, including default values, in lines that begin with “cfg_get_str”, “cfg_get_int”, “cfg_get_bool”, and so on. What’s really nice is that the syntax is perfect, and you can copy and paste it into your .cf file. This is how I figured out how to use result_attribute correctly. Now that postfix knows which domains it is hosting email for, we teach it which mailboxes exist. We start with more LDIF:

dn: ou=People,dc=example,dc=com
objectclass: organizationalUnit
objectclass: top
ou: People


dn: cn=Kai Meyer,ou=People,dc=example,dc=com
cn: Kai Meyer
cn: Master of the Universe
gidnumber: XXX
givenname: Kai
homedirectory: /home/kaiuser
loginshell: /bin/bash
mail: kaiuser@example.com
maillocaladdress: kaiuser@example2.com
maillocaladdress: kaiuser@example3.com
o: eSource
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: inetLocalMailRecipient
objectclass: posixAccount
sn: Meyer
telephonenumber: 555-555-5555
uid: kaiuser
uidnumber: XXX
userpassword: CENSORED

Notice, I have a “mail” and “maillocaladdress” attribute. “maillocaladdress” is a list of email aliases that will deliver to the “mail” address. With those LDAP entries in place, you can configure ldap_virtual_mailbox_maps.cf:

server_host = ldap://localhost/
search_base = ou=People,dc=example,dc=com
version = 3
bind = no
query_filter = (&(objectclass=inetOrgPerson)(mail=%s))
result_attribute = mail

So now I’m starting in the “People” tree (which means LDAP won’t even look in my other “Domains” tree) for all entries that are ObjectClass “inetLocalMailRecipient” and have a “mailLocalAddress” set to the lookup value. This is where email will actually attempt to deliver. IN my case above, it’s “kaiuser@example.com”. Again, before bothering postfix with a reload, you can do another postmap:

postmap -v -q kaiuser@example.com ldap:/etc/postfix/ldap_virtual_mailbox_maps.cf

Again, eliminate the -v if you just need a simple test. If you leave out the query_filter and result_attribute, postfix goes looking for objectclasses and attributes that don’t exist on my default openldap install, so again I’m modifying the default behavior to get the information I want it to look for. Hurray, mailboxes done. Now let’s do aliases. We don’t need any more LDAP entries. We created one with fields we can use for aliases already. Here’s ldap_virtual_alias_maps.cf:

server_host = ldap://localhost/
search_base = ou=People,dc=example,dc=com
version = 3
bind = no
query_filter = (&(objectclass=inetLocalMailRecipient)(mailLocalAddress=%s))
result_attribute = mail

Here, the result attribute is the same “mail”, but the filter is for “mailLocalAddress”. Doing another postmap for example2.com should return the email address for example.com, based on my LDIF above. Again, I wish I had LDAP attributes the way I want them (and I could modify/create schema to get it if I really really wanted to), but this is what I have to work with. So, postfix is done (for me anyway.) I don’t have any other MySQL lookups in my existing server. So now we need to plug in Dovecot.

CentOS 6 comes with Dovecot 2.0. I know that Dovecot2 is very different than Dovecot1, so I’m sorry if you’re stuck on 1. The changes to dovecot were actually quite minimal. Start in /etc/dovecot/conf.d/10-auth.conf, and un-comment the ldap include:

!include auth-ldap.conf.ext

Then copy the example ldap config to /etc/dovecot.conf:

cp /usr/share/doc/dovecot-2.0/example-config/dovecot-ldap.conf.ext /etc/dovecot

Then modify the following variables (they are all there either in comments or not):

hosts = localhost
auth_bind = no
ldap_version = 3
base = dc=example,dc=com
deref = never
scope = subtree
user_attrs =
user_filter = (&(objectclass=inetOrgPerson)(mail=%u))
pass_attrs = mail=user,userPassword=password
pass_filter = (&(objectclass=inetOrgPerson)(mail=%u))
default_pass_scheme = SSHA

With LDAP done, we just need to teach dovecot about how to dump the email the same way it was don the last machine, in /var/vmail/<email dress>. In /etc/dovecot/conf.d/10-mail.conf, add:

mail_location = maildir:/var/vmail/%u

Then we need to enable postfix-auth and auth-userdb correctly. In /etc/dovecot/conf.d/10-master.conf, change your “service auth” settings to this:

service auth {
  # auth_socket_path points to this userdb socket by default. It's typically
  # used by dovecot-lda, doveadm, possibly imap process, etc. Its default
  # permissions make it readable only by root, but you may need to relax these
  # permissions. Users that have access to this socket are able to get a list
  # of all usernames and get results of everyone's userdb lookups.
  unix_listener auth-userdb {
    mode = 0640
    user = vmail
    group = mail

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = mail

  # Auth process is run as this user.
  #user = $default_internal_user

And we’re done! ( I think ). I may have missed a step here or there. Leave a comment if you are running into a problem. Chances are, it’s one I hit, and solved.

Next on the chopping block, SVN and SuPHP.