Welcome to the DjaoDjin Blog!

A place to share experiences in building Software-as-a-Service.

Postfix, Dovecot and OpenLDAP on Fedora 21

by Sebastien Mirolo on Mon, 30 Mar 2015

So far we had run the mail server with static hash files. As we add contributors and machines in the mix, it is about time to introduce a central account service (i.e. OpenLDAP). Since Rackspace and Amazon deprecated their first generation instances, re-building the mail infrastructure from scratch on a new system seemed appropriate - plus, it is fun!

Following is a diagram of how we planned to hook-up the authentication for accessing inboxes and sending e-mails.

The idea is to configure PAM to authenticate users through LDAP, then have Dovecot (IMAP/POP) to rely on PAM authentication and Postfix (SMTP) rely on Dovecot pass through authentication.

Most setup deal with multiple e-mail addresses through alias maps and forwards but here, we wanted to have physically separate inboxes for each email address and a single user owning multiple addresses, as in:

dn: uid=johnd,ou=people,dc=mydomain,dc=com
objectClass: inetorgperson
cn: John Doe
mail: johnd@mydomain.com
mail: info@mydomain.com

This last requirement turned out in a puzzle of incompatible pieces to fit together. We did. Here is the story of that journey.

Setting up OpenLDAP

The first step was to bring-up OpenLDAP. OpenLDAP is notoriously complex to setup because the error messages are cryptic and often unrelated to the root cause of the issue.

We will install both the OpenLDAP servers, and clients - there is a high probability we will need to run some command lines to probe and test the setup.

$ yum install openldap-servers openldap-clients

By default slapd uses syslog to send information and log error messages. Since we are using the syslog-ng variant, we will add a file as such:

$ vi /etc/syslog-ng/conf.d/slapd.conf
destination d_ldap      { file("/var/log/slapd.log"); };
filter f_ldap           { program(slapd); };
log { source(s_sys); filter(f_ldap); destination(d_ldap); };

$ sudo systemctl restart syslog-ng

We are testing the setup on Fedora 21 here so we need to come back a bit on how OpenLDAP is compiled on Fedora systems. We will want to use ldaps:// (LDAP over TLS) which means we will have to configure keys and certificates. That is where things get serious. No matter how we configure the various certificate databases, we end-up with an error messages that read:

TLS: error: connect - force handshake failure: errno 0 - moznss error -5938
TLS: can't connect: TLS error -5938:Encountered end of file.
# or
TLS: cannot open certdb '/etc/openldap', error -8018:Unknown PKCS #11 error.

By default on Fedora, OpenLDAP is linked with the moznss implementation of TLS. You can check by running ldd

$ ldd /usr/sbin/slapd | grep nss
    libnss3.so => /lib64/libnss3.so (0x00007f621ed2a000)
    libnssutil3.so => /lib64/libnssutil3.so (0x00007f621d636000)

You might try to get moznss certificate database to work. If so, here and here are good starting points.

Fortunately, there are two other common open source implementation of TLS: openssl and gnutls. Now OpenSSH, Dovecot and Postifx all are linked with OpenSSL by default on Fedora. So you can see the use of MozNSS in OpenLDAP as either a security feature (a different code base) or as a security risk (a different code base). We just went ahead and recreated RPM packages that are linked against OpenSSL.

# 1. Download the source package and prerequisites
$ sudo yum install yum-utils rpmdevtools cracklib-devel cyrus-sasl-devel \
    groff krb5-devel libtool libtool-ltdl-devel nss-devel openssl-devel \
    tcp_wrappers-devel unixODBC-devel perl-ExtUtils-Embed
$ rpmdev-setuptree
$ yumdownloader --source openldap
$ rpm -i openldap-2.4.40-2.fc21.src.rpm

# 2. Update the spec file and rebuild the packages
$ diff -u prev ~/rpmbuild/SPECS/openldap.spec
-        --with-tls=moznss \
+        --with-tls=openssl \
$ rpmbuild -ba ~/rpmbuild/SPECS/openldap.spec

# 3. Install the RPMs
$ rpm -i ~/rpmbuild/RPMS/x86_64/openldap-2.4.40-2.fc21.x86_64.rpm --force
$ rpm -i ~/rpmbuild/RPMS/x86_64/openldap-servers-2.4.40-2.fc21.x86_64.rpm --force
$ rpm -i ~/rpmbuild/RPMS/x86_64/openldap-clients-2.4.40-2.fc21.x86_64.rpm --force

# 4. Check sldapd is linked against the expected libraries
$ ldd /usr/sbin/slapd | grep ssl
    libssl.so.10 => /lib64/libssl.so.10 (0x0000xxxxxxxxx000)

Sure enough the slapd service will not start right of the box. It can't open the bind ldapi:// to a socket. SELinux won't let it do so - easily fixed.

$ sudo sh -c 'grep slapd /var/log/audit/audit.log | audit2why -M slapd'
type=AVC msg=audit(1422216270.205:722): avc:  denied  { unlink } for  pid=1520\
    comm="slapd" name="ldapi" dev="tmpfs" ino=15555 \
    scontext=system_u:system_r:slapd_t:s0 \
    tcontext=unconfined_u:object_r:var_run_t:s0 tclass=sock_file permissive=0
$ sudo sh -c 'grep slapd /var/log/audit/audit.log | audit2allow -M slapd'
$ semodule -i slapd.pp
$ sudo rm slapd.pp slapd.te

At first we can configure OpenLDAP to point to the exact same key and cert files we are using for Dovecot and Postfix. That would work but it is not ideal when we are looking to extend the use of OpenLDAP to support single sign-on.

Since there are no reasons we would want the OpenLDAP server to be available on a public DNS, nor any reason unknown parties should talk to the server, we will use a self-signed certificate.

# Create private key and Certificate Signing Request (CSR)
$ sudo openssl req -new -sha256 -newkey rsa:2048 -nodes \
    -keyout /etc/pki/tls/private/ldap.key \
    -out /etc/pki/tls/certs/ldap.csr
Common Name (eg, your name or your server's hostname) []: Use private IP address here

# Self-signing
$ openssl x509 -req -days 365 -in /etc/pki/tls/certs/ldap.csr \
    -signkey /etc/pki/tls/private/ldap.key \
    -out /etc/pki/tls/certs/ldap.crt

There is not much we can do without a root user, or DN in LDAP terminology. Hence we run slappasswd to get a password hash we will copy/paste into the configuration files.

$ slappasswd
New password:
Re-enter new password:

OpenLDAP is versatile enough that it can update configuration files through regular LDAP command line client utilities. Figuring out how to that at this point is just more complex than need be so we just edit the config files in place, update the CRC values and restart the slapd daemon.

$ diff -u prev /etc/openldap/ldap.conf

-TLS_CACERTDIR   /etc/openldap/cacerts
+TLS_CACERT      /etc/pki/tls/certs/ldap.crt
+TLS_REQCERT     demand

$ diff -u prev /etc/openldap/slapd.d/cn=config/olcDatabase={0}config.ldif
+olcRootPW: {SSHA}password_hash

$ diff -u prev /etc/openldap/slapd.d/cn=config/olcDatabase={2}hdb.ldif
-olcSuffix: dc=my-domain,dc=com
-olcRootDN: cn=Manager,dc=my-domain,dc=com
+olcSuffix: dc=mydomain,dc=com
+olcRootDN: cn=Manager,dc=mydomain,dc=com
+olcRootPW: {SSHA}password_hash
+olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
+olcAccess: {0}to attrs=userPassword by self write by dn.base="cn=Manager,dc=mydomain,dc=com" write by anonymous auth by * none
+olcAccess: {1}to * by dn.base="cn=Manager,dc=mydomain,dc=com" write by self write by * read"

$ diff -u /etc/openldap/slapd.d/cn=config.ldif
-olcTLSCACertificatePath: /etc/openldap/certs
-olcTLSCertificateFile: "OpenLDAP Server"
-olcTLSCertificateKeyFile: /etc/openldap/certs/password
+olcTLSCACertificatePath: /etc/pki/tls/certs
+olcTLSCertificateFile: /etc/pki/tls/certs/ldap.crt
+olcTLSCertificateKeyFile: /etc/pki/tls/private/ldap.key

We want to get rid of the warnings in the log file so we update the CRC32 after modifying the config files in place. To recompute the CRC32 we use the following command:

$ yum install perl-String-CRC32
$ tail -n +3 /etc/openldap/slapd.d/config-file \
    | perl -e 'use String::CRC32; printf("%x\n", crc32(\*STDIN));'

It is time to start the OpenLDAP daemon and install standard schemas to describe users and POSIX accounts.

$ systemctl enable slapd
$ systemctl start slapd

# RFC 4524
$ ldapadd -x -H ldap:/// -f /etc/openldap/schema/cosine.ldif -D "cn=config" -W
# still necessary though might be deprecated ...
$ ldapadd -x -H ldap:/// -f /etc/openldap/schema/nis.ldif -D "cn=config" -W
# RFC 2798
$ ldapadd -x -H ldap:/// -f /etc/openldap/schema/inetorgperson.ldif \
    -D "cn=config" -W

Let's now add our first user to the directory. In the process we will add required top domain and groups. A good read at this point is Specifying Directory Entries Using LDIF

$ cat people.ldif
# Without creating a top level organization we might end-up with errors like:
# ldap_add: Insufficient access (50)
#    additional info: no write access to parent

dn: dc=mydomain,dc=com
objectclass: top
objectclass: domain
dc: mydomain
description: Example inc.

dn: ou=people,dc=mydomain,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people
description: People with credentials on mydomain infrastructure

dn: ou=groups,dc=mydomain,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups
description: Unix groups on mydomain infrastructure

dn: uid=johnd,ou=people,dc=mydomain,dc=com
objectClass: inetorgperson
objectClass: posixaccount
sn: Doe
cn: John Doe
uid: johnd
uidNumber: 1000
gidNumber: 1000
homedirectory: /home/johnd
shell: /bin/bash
mail: johnd@mydomain.com
mail: info@mydomain.com

dn: cn=wheel,ou=groups,dc=mydomain,dc=com
cn: wheel
objectClass: top
objectClass: posixGroup
gidNumber: 10
memberUid: johnd

dn: cn=dev,ou=groups,dc=mydomain,dc=com
cn: dev
objectClass: top
objectClass: posixGroup
gidNumber: 1000
memberUid: johnd

$ ldapadd -x -H ldap:/// -f people.ldif \
    -D "cn=Manager,dc=mydomain,dc=com" -W

# Let's check we can access the newly created user 4 different way:
$ ldapsearch -xLLL -b "dc=mydomain,dc=com" uid=johnd
$ ldapsearch -xLLL -D uid=johnd,ou=people,dc=mydomain,dc=com
$ ldapsearch -xLLL -b dc=mydomain,dc=com \
    -D uid=johnd,ou=people,dc=mydomain,dc=com
$ ldapwhoami -x -D uid=johnd,ou=people,dc=mydomain,dc=com -W

On a side note, to export the ldap directory tree into ldif format (very useful on system upgrades), we can use slapcat as such:

$ slapcat -v -l people.ldif

Though before uploading the ldif file back, you will need to prune a bunch of generated fields out of the dump. Failing to do so, you end up with Constraint violation errors.

ldap_add: Constraint violation (19)
	additional info: structuralObjectClass: no user modification allowed

Until now we used the ldap:// port. It is time to turn on TLS and test connection through the secure ldaps:// port. After we replaced moznss by openssl, most errors are straightforward to fix. They are either related to the SELinux configuration or to the domain name associated to the EC2 instance.

$ service slapd restart
# SELinux
TLS: could not use key file `/etc/pki/tls/private/ldap.key'.
TLS: error:0200500D:system library:fopen:Permission denied bss_file.c:393

There seems to be a bug in OpenLDAP because it will require the private key to be accessible by the user passed on the slapd command line (ex: -u lapd).

$ sudo chmod 750 /etc/pki/tls/private
$ sudo chmod 640 /etc/pki/tls/private/ldap.key
$ sudo chgrp -R ldap /etc/pki/tls/private

$ ldapsearch -d -1 -xLLL  -H ldaps:// -b "dc=mydomain,dc=com" uid=johnd
# domainName
TLS: hostname (ip-aa-bb-cc-dd.us-east-2.compute.internal) does not match \
    common name in certificate (*.mydomain.com).
TLS: can't connect: TLS: hostname does not match CN in peer certificate.

This error will appear because we used the Private IP address in the Certificate Signing Request, not the private domain name. We thus tweak the command as in:

$ ldapsearch -d -1 -xLLL  -H ldaps://PrivateIP/ -b "dc=mydomain,dc=com" uid=johnd

It doesn't hurt to associate the public hostname with the loopback either so requests for mail.mydomain.com initiated locally do not wander around.

$ diff -u prev /etc/hosts
- localhost
+ mail.mydomain.com localhost

Setting up Postfix

$ sudo yum install postfix
$ sudo systemctl enable postfix
$ sudo systemctl start postfix

Postfix takes a while to get used to but once you know your way around, is pretty straightforward to bend to your exact will.

There are plenty of posts on setting up postfix. We will skip here through some of the details and focus on creating virtual mailboxes. All mailboxes are own by a vmail user account. We are only interested to use OpenLDAP for virtual_mailbox_maps here.

$ sudo adduser -u 5000 -M -s /sbin/nologin vmail
$ sudo mkdir -p /var/db/mail/vhosts/mydomain.com
$ sudo chown vmail:vmail /var/db/mail/vhosts/mydomain.com

$ diff -u prev /etc/postfix/main.cf
+virtual_mailbox_domains = mydomain.com
+virtual_mailbox_base = /var/db/mail/vhosts
+virtual_mailbox_maps = ldap:/etc/postfix/ldap-vmailbox
+virtual_minimum_uid = 100
+# vmail:vmail equivalent to 5000:5000
+virtual_uid_maps = static:5000
+virtual_gid_maps = static:5000
+virtual_alias_maps = hash:/etc/postfix/virtual

$ diff -u prev /etc/postfix/virtual
postmaster@mydomain.com   postmaster
@mydomain.com             info@mydomain.com

$ sudo postmap /etc/postfix/virtual

$ diff -u prev /etc/postfix/ldap-vmailbox
server_host = localhost
version = 3
search_base = ou=people, dc=mydomain, dc=com
query_filter = mail=%s
result_attribute = mail
result_format = %d/%u

At this point we test we can retrieve maildrop directories from LDAP.

$ postmap -q johnd@mydomain.com ldap:/etc/postfix/ldap-vmailbox
mydomain.com/johnd/, mydomain.com/info/

We get both maildrops. That is a problem though it is behaving as documented in LDAP_TABLE (5). It seems impossible to use the actual key that was used for the lookup as the result of the query. We will have to rely on hash files. Unfortunately in postconf (5) it is stated:

Note 1: for security reasons, the virtual(8) delivery agent disallows regular expression substitution of $1 etc. in regular expression lookup tables, because that would open a security hole.

It means we must add each mapping explicitly in the vmailbox hash file. (My personal opinion is that there were alternative ways of insuring security outside simply and purely disabling regular expressions...)

$ diff -u prev /etc/postfix/main.cf
+virtual_mailbox_maps = hash:/etc/postfix/vmailbox

$ diff -u prev /etc/postfix/vmailbox
johnd@mydomain.com     mydomain.com/johnd/
info@mydomain.com      mydomain.com/info/

$ sudo postmap /etc/postfix/vmailbox

Moving on to testing with the local sendmail.

"Unable to locate mail" is an error that baffled me for quite some time until I finally realize spamc was not using the postfix sendmail as I was expected but ssmtp sendmail instead.

$ tail /var/log/maillog
sSMTP[3357]: Unable to locate mail
sSMTP[3357]: Cannot open mail:25
postfix/pipe[3356]: B1D1B4035B: to=<johnd@mydomain.com>,\
    relay=spamassassin, delay=0.76, delays=0.6/0.01/0/0.15, dsn=5.3.0,\
    status=bounced (Command died with status 1: "/usr/bin/spamc".\
    Command output: sendmail: Cannot open mail:25 )

That is fixed by changes the default mta alternative.

$ ls -la /etc/alternatives/mta
    /etc/alternatives/mta -> /usr/sbin/sendmail.ssmtp
$ alternatives --display mta
$ alternatives --config mta
# or
$ alternatives --set mta /usr/sbin/sendmail.postfix
$ ls -la /etc/alternatives/mta
    /etc/alternatives/mta -> /usr/sbin/sendmail.postfix

Another warning we found annoying and wanted to fix is an untrusted TLS connection to gmail.

$ tail /var/log/maillog
postfix/smtp[3788]: Untrusted TLS connection established \
    to gmail-smtp-in.l.google.com[]:25:\
    TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)

First we test we have a correct CA bundle to connect to gmail.

$ openssl s_client -showcerts -CAfile /etc/pki/tls/certs/ca-bundle.crt \
    -starttls smtp -connect gmail-smtp-in.l.google.com:25

Then we modify our postfix configuration to use this CA bundle file. We also force connection to gmail to be verified.

$ diff -u prev /etc/postfix/main.cf
+smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt
+smtpd_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt

$ vi /etc/postfix/tls_policy
gmail.com  verify
.gmail.com verify

$ sudo postmap /etc/postfix/tls_policy

Adding a SPF record

At this point we are able to send and receive mail. To prevent SPAM filters to miscategorize mail coming from our server, we add a SPF record to the DNS record.

We allow A records, domain's MXes and amazon SES (since the webapp relies on SES) to send mail for the domain, prohibit all others.

# TXT Records (Optional Text Fields)
"v=spf1 a mx include:amazonses.com -all"

OK. Time to access our e-mails from an external client.

Setting up Dovecot

$ yum install dovecot libtool-ltdl
$ systemctl enable dovecot
$ systemctl start dovecot

Dovecot spits out very informative error messages so it is straightforward to walk through what needs to be fixed after the initial configuration. Here again SELinux is often the culprit. On modern Linux distributions, it is a good habit to always check in /var/log/audit/audit.log when something looks odd.

$ grep dovecot /var/log/audit/audit.log | audit2why -M dovecot

# Dovecot and SELinux
$ sudo semanage fcontext -a -t dovecot_spool_t "/var/db/mail/vhosts(/.*)?"
$ sudo restorecon -R -v /var/db/mail/vhosts

Here we hit a wall to configure authentication through PAM while allowing multiple e-mail inboxes per users.

There is three ways to connect to a LDAP directory from PAM, the old way and two new ways. Unfortunately after hours of fiddling with each of them, they fall short of delivering the expected result. To make it quick:

  • pam_ldap has a pam_login_attribute for retrieving a directory entry. It is though a deprecated package on Fedora and must be compiled from source. it has been written off as leaking memory and buggy beyond repair.
  • nss-pam-ldapd is most likely the first choice you will find when searching for "pam ldap", though Fedora prefers their sssd implementation as a default. For us the issue is the lack of pam_login_attribute option. Even with pam_regex we couldn't bypass that limitation (because of a 3-way interaction with Dovecot).
  • sssd is heavy on local caching (its selling point) but is either buggy by design or by accident. It will not support to index its cache by the key you use for retrieving a directory entry. Instead it will insists on using the first e-mail address associated with an entry.

Dovecot allows to authenticate directly against a LDAP directory, so finally, that is the configuration we implemented.

$ diff -u prev /etc/dovecot/conf.d/10-auth.conf
-!include auth-system.conf.ext
-#!include auth-ldap.conf.ext
+#!include auth-system.conf.ext
+!include auth-ldap.conf.ext

$ diff -u prev /etc/dovecot/conf.d/10-mail.conf
-#mail_location =
+mail_location = maildir:/var/db/mail/vhosts/%d/%n

$ diff -u prev /etc/dovecot/conf.d/10-master.conf
   # Postfix smtp-auth
-  #unix_listener /var/spool/postfix/private/auth {
-  #  mode = 0666
-  #}
+  unix_listener /var/spool/postfix/private/auth {
+    mode = 0666
+    user = postfix
+    group = postfix
+  }

$ diff -u prev /etc/dovecot/conf.d/10-ssl.conf
-ssl_cert = mydomain.com.crt
+ssl_key = mydomain.com.key

$ diff -u /etc/dovecot/conf.d/auth-ldap.conf.ext
-userdb {
-  driver = ldap
-  args = /etc/dovecot/dovecot-ldap.conf.ext
+#userdb {
+#  driver = ldap
+#  args = /etc/dovecot/dovecot-ldap.conf.ext
-#userdb {
-#  driver = static
-#  args = uid=vmail gid=vmail home=/var/vmail/%u
+userdb {
+  driver = static
+  args = uid=vmail gid=vmail home=/var/db/mail/vhosts/%d/%n

$ vi /etc/dovecot/dovecot-ldap.conf.ext
# Both slapd and dovecot are running on the same machine. We rely on
# on the clear text ldap:// port and localhost here in case the domain
# name for the machine is reset incorrectly somewhere in a reboot sequence.
uris = ldap://localhost/
ldap_version = 3
tls_ca_cert_file = /etc/pki/tls/certs/ldap.crt

base = dc=mydomain,dc=com

auth_bind = yes
auth_bind_userdn = uid=%n,ou=people,dc=mydomain,dc=com

user_filter = (&(objectClass=posixAccount)(mail=%u))

We are not believers in STARTTLS, so we disable Dovecot listening on clear text POP and IMAP ports.

$ diff -u prev /etc/dovecot/conf.d/10-master.conf
 service imap-login {
   inet_listener imap {
-    #port = 143
+    port = 0

 service pop3-login {
   inet_listener pop3 {
-    #port = 110
+    port = 0

After configuring all of the email stack, we try to send and receive e-mails from the OSX Mail.app client. Here weird connections to Dovecot IMAP or POP can often be solved by a process of "quit and restart Mail.app".


As usual with software, a lot of time went into figuring out the politics, community and changing landscape behind various pieces of the puzzle. I hope this article has been helpful to you.

Side Notes

Newest approach to PAM-LDAP authentication will pass through an intermediate generic secure authentication daemon, either sssd or nslcd.


The log output that made me believe sssd is using a different key for retrieving and LDAP entry and indexing its cache.

[sssd[be[default]]] [sdap_get_generic_ext_step] (0x0400): \
    calling ldap_search_ext with [(&(mail=info@mydomain.com\
[sssd[be[default]]] [sdap_save_user] (0x0400): Save user
[sssd[be[default]]] [sdap_save_user] (0x0400): Processing user johnd
[sssd[be[default]]] [sdap_save_user] (0x0400): Adding user principal\
    [johnd@mydomain.com] to attributes of [johnd].
[sssd[be[default]]] [sysdb_get_real_name] (0x0040): Cannot find user\
    [info@mydomain.com] in cache
[sssd[be[default]]] [sysdb_search_by_name] (0x0400): No such entry

Some posts on sssd worth reading:

Switching from SSSD to NSS-PAM

SSSD is the default authentication daemon on Fedora 21. Though I don't know any reasons to switch to nslcd at this point, the instructions to do so are pretty straightforward.

$ sudo yum install nss-pam-ldapd
$ diff -u prev /etc/nslcd.conf
-base dc=mydomain,dc=com
+base dc=mydomain,dc=com

$ sudo systemctl enable nslcd
$ sudo systemctl start nslcd

$ diff -u prev /etc/nsswitch.conf
-passwd:     files sss
-shadow:     files sss
-group:      files sss
+passwd:     files ldap
+shadow:     files ldap
+group:      files ldap

Compiling PADL pam_ldap

Compiling pam_ldap requires cvstools, also provided by PADL.

Modifying LDAP entries

Here is an example on how to use ldapmodify to update an LDAP entry.

$ cat john.ldif
dn: uid=john,ou=people,dc=mydomain,dc=com
changetype: Modify
add: loginshell
loginshell: /bin/sh

$ ldapmodify -x -H ldap:/// -f johnd.ldif \
    -D "cn=Manager,dc=mydomain,dc=com" -W
$ ldapsearch -xLLL -b "dc=mydomain,dc=com" uid=johnd