Enterprise mail routing with Postfix and LDAP

A LDAP directory offers a single, logically centralized, hierarchical store to keep data, like information about users, groups, mailboxes, services, etc. There are a number of applications out there than can leverage this LDAP store. For example, PAM can authenticate by checking against an LDAP directory, Cyrus-IMAP can check whether a certain user has a IMAP/POP mailbox available, and Postfix can contact an LDAP directory in order to route an incoming mail to its corresponding mail host.

LDAP is flexible enough to describe abstract concepts, like persons, by using object classes. Each object class is built upon attributes. An attribute is the minimal unit of information and tells about a property, such as the name of a person, its postal address or e-mail address. The minimal storage unit is an LDAP entry, however, which is an instance of one ore several object classes. The collection of attributes and object classes is called schema, while the collection of LDAP entries is called Directory Information Tree (DIT).

There are several RFC documents out there describing most of the standard LDAP object classes for the LDAPv3 protocol. There are other object classes which are either propietary or non-standard. For example, Fedora Directory Server defines an object class named mailRecipient. This legacy object class was used by the Netscape Messaging Server 4 to define a mailbox and is defined as:

objectclasses: ( 2.16.840.1.113730.3.2.3
NAME 'mailRecipient'
DESC '' SUP top AUXILIARY
MUST ( objectClass )
MAY ( cn $ mail $ mailAlternateAddress $
mailHost $ mailRoutingAddress $
mailAccessDomain $ mailAutoReplyMode $
mailAutoReplyText $ mailDeliveryOption $
mailForwardingAddress $
mailMessageStore $ mailProgramDeliveryInfo $
mailQuota $ multiLineDescription $ uid $
userPassword )
X-ORIGIN 'Netscape Messaging Server 4.x' )

This object class is particularly simple and interesting since it defines a few concepts we can use when routing mail, like the idea of multiple mail aliases for a single mailbox.

Describing our purpouses

Take this scenario: we have a real user, John Smith which owns a mailbox which is stored at host mail1.internal. The mailbox for John Smith is john.smith@mail1.internal, since it’s stored in host mail1.internal. Let’s say this real user has the following e-mail addresses assigned to him (assigned to its mailbox):

  • sample.user@example.com
  • sample_user@example.com
  • sampleuser@example.com
  • sample@example.com
  • john.smith@example.com

These are the e-mail addresses. The last e-mail address is, in fact, the real e-mail address that anyone could expect John Smith to have. The other e-mail addresses are in fact aliases.

Whenever a Postfix MTA in the enterprise receives a mail for any of these e-mail addresses, we want it to be delivered to a mailbox named john.smith in host mail1.internal, that is, delivered to john.smith@mail1.internal, and so, john.smith@mail1.internal is the real address where messages sent to John Smith, using any of the previous e-mail addresses, must be routed to. Thus, john.smith@mail1.internal is what we will call the mail routing address.

We will configure Postfix to use an LDAP-based virtual alias map. Each time a mail is sent to Postfix, it will perform an LDAP query for that e-mail address and will try to guess its mail routing address. If one is found, Postfix will deliver that mail to it:

  • If the domain part (the address at the right of the @ sign) of the mail routing address equals the FQDN of the Postfix host:

    Postfix will try to deliver the message locally, usually using an MDA, like procmail or cyrus-imapd/deliver (Cyrus-IMAP local delivery agent used to deliver messages to a IMAP mailbox).

    For example, let be john.smith@mail1.internal the mail routing address and mail1.internal Postfix’s hostname FQDN. In this case, Postfix will deliver the message locally using a MDA.

  • If the domain part (the address at the right of the @ sign) of the mail routing address does not equal the FQDN of the Postfix host:

    Postfix will trigger the message routing process again in order to deliver it to its final destination, checking querying the LDAP directory again if necessary.

To store mail routing addresses and it’s corresponding mail aliases, we will use the mailRecipient object class. The multi-valued mail attribute will hold all mail aliases and mailRoutingAddress will the real, final destination for any of them.

A sample entry, exemplifying the John Smith user we described before, in LDIF syntax is:

dn: uid=john.smith,ou=People, dc=example, dc=com
givenName: John
sn: Smith
mail: sample.user@example.com
mail: sample_user@example.com
mail: sampleuser@example.com
mail: sample@example.com
mail: john.smith@example.com
objectClass: top
objectClass: mailRecipient
uid: john.smith
mailRoutingAddress: john.smith@mail1.internal

Note the absence of the posixAccount object class. This means John Smith is not a regular UNIX user and thus, this mailbox cannot be used to log in through PAM, for example. Note the absence of the inetOrgPerson object class too, which could mean this mailbox is not assigned to a real user.

Configuring Postfix

The first thing that is required is creating a configuration file with details about how the LDAP directory server should be queried and contacted. The name of this file is not relevant, but I decided to name it /etc/postfix/ldap-aliases.cf:

# cat /etc/postfix/ldap-aliases.cf
bind = no
version = 3
timeout = 20

## set the size_limit to 1 since we only
## want to find one email address match
size_limit = 1
expansion_limit = 0

start_tls = no
#tls_require_cert = no

server_host = ldap://ldap1.internal/ ldap://ldap2.internal/
search_base = dc=example,dc=com
scope = sub
query_filter = (&(objectclass=mailRecipient)(mail=%s))
result_attribute = mailRoutingAddress
special_result_filter = %s@%d

The configuration options are described in detail in the Postfix ldap_table(5) manual page. A brief description lies hereafter:

  • bind = no tells Postfix to perform an unauthenticated (anonymous) BIND against the LDAP directory.
  • size_limit = 1 tells Postfix to request one, and only one, LDAP entry matching the query_filter, starting the search operation at the LDAP entry whose DN is specified by search_base, and using a scope search scope.

    The search scope is one of: sub, one, base.

  • server_host defines one or several LDAP hosts, trying them in order should the first one fail.
  • query_filter defines the LDAP search that Postfix will use in order to retrieve the mail routing address given a mail address.

    In the previous configuration file, the LDAP search filter will look for entries belonging to the mailRecipient object class whose mail attribute matches the recipient mail address of the incoming message.

    If one is found (at most only one entry will be retrieved since size_limit = 1), the mailRoutingAddress attribute, defined to by result_attribute, points to the final destination for the message, that is the mail routing address.

Finally, we will add the virtual_alias_maps directive to Postfix’s /etc/postfix/main.cf configuration file:

# tail -1 /etc/postfix/main.cf
virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf

For example, when receiving a message for sample.user@example.com, Postfix will first look to see if this mail address is an alias or a real address. The LDAP query can be tested by running:

$ ldapsearch -x -b"dc=example,dc=coml" 
"(&(objectclass=mailRecipient)(mail=sample.user@example.com))" 
mailRoutingAddress
# extended LDIF
#
# LDAPv3
# base  with scope sub
# filter: (&(objectclass=mailRecipient)(mail=sample.user@example.com))
# requesting: mailRoutingAddress
#

dn: uid=john.smith,ou=People, dc=example, dc=com
mailRoutingAddress: john.smith@mail1.internal

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

Thus, Postfix will route any message to sample.user@example.com to a mailbox named john.smith at mailhost mail1.internal.

User management using LDAP and libuser

libuser is a collection of libraries and tools to manage users and groups under Linux or UNIX-like systems. libuser has several modules allowing to manage users and groups through different backends, like local (password and shadow) and LDAP (the LDAP backend has been somewhat unusable up to version 0.52).

The LDAP module, however, requires the user to supply a BIND_PW (password) for the BIND_DN (user) in order to authenticate against the LDAP directory server an perform the modifications. In automated deployment scenarios, it’s essential to be able to perform operations on to the LDAP directory server without asking the user for the password. Instead, the password used to authenticate against the LDAP directory server could be well stored in libuser’s configuration file, /etc/libuser.conf.

I’ve made a patch against libuser that implements a new configuration parameter for the LDAP module. This new parameter is named password, and specifies the password for the user binddn user.

Here is a sample of libuser’s configuration file, /etc/libuser.conf:

[defaults]
# The default (/usr/lib*/libuser) is usually correct
# moduledir = /your/custom/directory
skeleton = /etc/skel
mailspooldir = /var/mail
modules = ldap
create_modules = ldap
crypt_style = md5

[userdefaults]
LU_USERNAME = %n
LU_UIDNUMBER = 10000
LU_GIDNUMBER = %u
LU_HOMEDIRECTORY = /home/%n
LU_LOGINSHELL = /bin/bash

[groupdefaults]
LU_GROUPNAME = %n
LU_GIDNUMBER = 10000

[ldap]
# Setting these is always necessary.
server = ldap://directory.server.fqdn
basedn = dc=example,dc=com

# Setting these is rarely necessary, since it's usually correct.
userBranch = ou=People
groupBranch = ou=Groups

# Set only if your administrative user uses simple bind operations to
# connect to the server.
binddn = cn=Directory Manager
password = secret

The patched SRPM and diff patch can be obtained from here: libuser-0.52.5-1.el4.1

Renaming an LDAP entry

The modrdn LDAP operation allows an authorized user to rename an LDAP entry’s RDN (that is, modifying the RDN of that entry).

Optionally, the modrdn operation can keep the old attributes that form the pristine RDN. This can be accomplished by specifiying deleteOldRDN:0 at the end of the modrdn data. If deleteOldRND:1 is specified at the end of the modrdn operation, or it is not specified at all, the modrdn operation will keep the attributes (and its values) that formed the pristine RDN.

For example, let’s add a sample entry:

$ ldapmodify ...
dn:cn=John Smith,ou=People,dc=sample,dc=com
changeType:add
objectClass:top
objectClass:person
cn:John Smith
sn:Smith

The attributes for the newly added entry are:

$ ldapsearch -x 
  -b"cn=John Smith,ou=People,dc=sample,dc=com" 
  -s base
dn: cn=John Smith,ou=People,dc=sample,dc=com
objectClass: top
objectClass: person
cn: John Smith
sn: Smith

Now, using the ldapmodify command, let’s invoke the modrdn operation onto the sample entry:

$ ldapmodify ...
dn:cn=John Smith,ou=People,dc=sample,dc=com
changeType:modrdn
newrdn:cn=John A. Smith
deleteOldRDN:1

Since deleteOldRND:1 has been specified, the old cn attribiute (commonName), which was part of the RDN, is removed and then replaced by the new cn attribute and it’s new value.

$ ldapsearch -x 
  -b"cn=John A. Smith,ou=People,dc=sample,dc=com" 
  -s base
dn: cn=John A. Smith,ou=People,dc=sample,dc=com
objectClass: top
objectClass: person
sn: Smith
cn: John A. Smith

Should have we specified deleteOldRND:0, then the entry would have looked as follows:

$ ldapsearch -x 
  -b"cn=John A. Smith,ou=People,dc=sample,dc=com" 
  -s base
dn: cn=John A. Smith,ou=People,dc=sample,dc=com
objectClass: top
objectClass: person
cn: John Smith
cn: John A. Smith
sn: Smith

Automatic start-up for SSL-enabled instances of Fedora Directory Server

Fedora Directory Server protects its internal, software-based, cryptographic repository with a PIN (passphrase).

When an instance of a Fedora Directory Server is configured for SSL/TLS support, by default, the start-up script interactively prompts for that PIN in order to unlock the private key. This can be a problem for automated system start-ups.

However, there is a way to configure Fedora Directory Server in such a way that the PIN is stored into a root-only readable configuration file. Thus, during start-up, the directory server instance can retrieve the PIN from that configuration file wihout asking it.

The PIN is stored into a file called:

/opt/fedora-ds/alias/slapd-[instance_name]-pin.txt

and should contain a single line with the following format:

Internal (Software) Token:[pin or passphrase]

For example, if the Fedora Directory Server instance is named "server1" and the PIN or passphrase needed to unlock the SSL/TLS private key is "secret":

# echo "Internal (Software) Token:secret" > 
   /opt/fedora-ds/alias/slapd-server1-pin.txt

Relative Distinguised Name

From RFC2251, Section 4,6, “Modify Operation”:

The Modify Operation cannot be used to remove from an entry any of its distinguised values, those values which form the entry’s relative distinguised name. An attempt to do so will result in the server returning the error notAllowedOnRDN. The Modify DN Operation described in section 4.9 is used to rename an entry.

From RFC2253, Section 2,2, “Converting RelativeDistinguishedName”:

When converting from an ASN.1 RelativeDistinguishedName to a string, the output consists of the string encodings of each AttributeTypeAndValue (according to 2.3), in any order.

Where there is a multi-valued RDN, the outputs from adjoining AttributeTypeAndValues are separated by a plus (‘+’ ASCII 43) character.