Enterprise mail routing with Postfix and LDAP
May 24th, 2006
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 = notells Postfix to perform an unauthenticated (anonymous) BIND against the LDAP directory.size_limit = 1tells Postfix to request one, and only one, LDAP entry matching thequery_filter, starting the search operation at the LDAP entry whose DN is specified bysearch_base, and using ascopesearch scope.The search scope is one of: sub, one, base.
server_hostdefines one or several LDAP hosts, trying them in order should the first one fail.query_filterdefines 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 byresult_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 # basewith 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
May 18th, 2006
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
April 19th, 2006
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
February 14th, 2006
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
November 4th, 2005
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.