Postfix makes a great mail gateway to a backend Exchange server. Sure, with Exchange 2007 and an edge role server you don’t have to, but unless you need to handle your message filtering yourself (see earlier posts on SaaS and message filtering), why buy extra licenses for something you can do very efficiently on open-source platforms?

You will find a number of sources of good advice for fronting Exchange with Postfix on the Internet, and also in The Book of Postfix by Ralf Hildebrandt and Patrick Koetter. I want to touch on the initial configuration briefly, and then focus on providing Postfix with a definitive list of relay recipients — all valid e-mail addresses which can be accessed from the outside. Why is this important? Without it, your mail gateway will forward all e-mail to your internal Exchange server for processing, even if the destination addresses are invalid, and this can represent a significant load. To eliminate this load, you have a couple of choices:

  1. Use a SaaS provider for message filtering, and:
    • Give that service a definitive list of valid e-mail addresses so that mail addressed to invalid addresses can be rejected there.
    • Reject all other SMTP connections through the MTA configuration on your mail gateway, or at your firewall. Not all senders will honor your DNS MX resource records, so this is still necessary!
  2. Use a setup as described below. Even if you use the first option, you still may want to employ the method below for defense-in-depth, but if you don’t and you run your own filtering service, e.g. SpamAssassin, you will definitely want to restrict forwarding for invalid recipients at your mail gateway.

Chris Covington provided a simple perl script that served as a starting point, but this will be extended with additional and important functionality. Links to the complete new script, the configuration file, and shell script for automated processing on the mail gateway are provided below.

Initial Setup

A rather simple configuration for Postfix as a mail gateway (aka relay or “smarthost”) will relay all mail for your domain to the internal SMTP server. Unless otherwise noted, all the parameters are in (typically in /etc/postfix). Make a backup copy of this file (e.g. prior to beginning your edits as outlined in the steps below.

  1. Configure relay_domains to match the domains for which this host should relay mail to an inside host:
    relay_domains =
  2. If you have mydestination configured, make it empty so that Postfix is not a final destination for mail. Any domains listed here should now be in listed relay_domains.
    mydestination =
  3. Make local_recipient_maps empty to disable delivery to local mailboxes.
    local_recipient_maps =
  4. Make sure myorigin is set to [one of] your internal domain(s). Since the intent is to disable local delivery, any messages sent from local process would appear to come from this host, making it impossible to reply to them (which you probably don’t want anyway, but it could cause confusion).
    myorigin =
  5. Forward any required local recipients to internal addresses. RFC2412 suggests many standard addresses for various functions, most of which are either suggested or mandated by other RFCs, e.g. postmaster and abuse. To do this, you must set the virtual_aliases_maps parameter, create the corresponding file with name/value pairs, and build an indexed map from it.
    • A typical name for such a map is “virtual”, which might make the complete pathname to the file /etc/postfix/virtual. The entries would look like:
    • Build the indexed map using the command:
      postmap hash:/etc/postfix/virtual
    • Add the required configuration in like this:
      virtual_alias_maps = hash:/etc/postfix/virtual
  6. Prevent Postfix from starting the local delivery agent by commenting out the local service (the line beginning with “local”) in the configuration file. Then reload Postfix using the postfix reload command.

Configuring Valid Recipients from Exchange

Now you are ready to extract a list of valid recipients from Exchange in the proper format for a relay recipient map, copy that file to the mail gateway, create the indexed map and load it. After you’ve done this, the procedure can be automated so that as you add new valid recipients to Exchange, they will be allowed to receive e-mail from the Internet.

Setup the Script for the Extraction of Valid SMTP Addresses

First, an initial assumption is required. I did not want to run the script to extract the valid list of relay recipients directly on the Exchange server, or on an Active Directory domain controller. If you do, see “The Book of Postfix” for a complete example of how to do this using csvde. I preferred to run the extraction process on a Linux box for a couple of reasons:

  1. Rather than exporting all SMTP addresses from Exchange, it is preferable to export only addresses which do not require sender authentication. By definition, addresses requiring sender authentication, e.g. internal mailing lists, cannot receive e-mail from the Internet, so it is better to eliminate them altogether. This precludes the use of something as simple as csvde; I chose perl. The perl script could be run from a Windows box provided that the required perl modules are present and the script is modified to support Putty SCP (pscp) instead of standard Unix scp.
  2. I prefer using cron for scheduling because of the more flexible scheduling options, and natural integration with e-mail for notification.

Download the script and the template configuration file. The configuration file should be located in the /etc/postfix directory (if you don’t run postfix on this box it won’t hurt to create it; otherwise modify the script to load the configuration file from wherever you place it). Now, modify the configuration file for your environment. The file includes comments to help you. Do not modify the stanza names (in square brackets). A few of these parameters deserve a bit of extra explanation. If you host other internal domains which should not be represented on the mail gateway, use the exclDomains parameter to exclude them from the extraction. If you want the script to remove e-mail addresses for these domains, turn on the delExcludes parameter; any address found that matches those domains will be removed from Active Directory; the user DN or UPN specified must have write access to your Active Directory. Use with caution! The others should be clear, or will become clear shortly.

# Configuration data for /etc/

[ AD ]

    # One server per line; use a backslash to continue to next line
    # Use server:port to specify a non-standard or SSL port
    DCs			= \

    # Default port
    defPort		= 389

    # Timeout in seconds
    timeout		= 5

    # Base DN for search
    baseDN		= dc=example,dc=com

    # LDAP search filter
    # A minimal filter might be (&(sAMAccountName=*)(mail=*))
    searchFilter	= \
(& (mailnickname=*) (| (&(objectCategory=person) \
(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*))) \
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*) \
(msExchHomeServerName=*)))(&(objectCategory=person) \
(objectClass=contact)) (objectCategory=group) \
(objectCategory=publicFolder) \
(objectClass=msExchDynamicDistributionList) ))

    # Bind anonymously (0|1); if set then user/passwd are ignored
    bindAnon		= 0

    # User may be a DN or a UPN name, e.g. user\
    user		= cn=user,ou=Users,dc=example,dc=com
    passwd		= secretpassword


    # Comma-separated domains to exclude
    # Escape periods, e.g. example\.com
    # Single line per domain; backslash to continue
    exclDomains		= \

    # Delete invalid addresses of those match excluded domains (0|1)
    delExcludes		= 0

    # Output file
    outFile		= /etc/postfix/relay_recipients

[ SCP ]

    # Username to be used for scp to mail gateway in DMZ
    scpUser		= postrelay

    # Remote host (mail gateway)
    mailGW              =

    # Private key
    identityFile	= /home/postrelay/.ssh/id_dsa
Create user for secure copy on mail gateway

Although there are several ways to script securely moving files from one host to another, using asymmetric encryption is probably the best. The first step to accommodate this is to create a user for receiving the files on the mail gateway. Check your manpages for the invocation of the appropriate command and proper syntax, e.g. useradd or adduser. After you’ve tested for correct behavior, you can disable local logins.

Create authentication keys

You can use one of the two methods below for generating the public-private key pair. IMPORTANT: Do not enter a passphrase for the key. If you do, your copy operations cannot be scripted, since the passphrase will have to entered when the key is used. A blank passphrase simply relies on one side possessing the private key while the other side has the public key.

  1. Generate the key pair on a system with ssh.
    • Use the command ssh-keygen -t rsa to generate a 2048-bit RSA key.
  2. The puttygen tool on Windows as part of the Putty package.
    • Launch puttygen.
    • Select the SSH-2 RSA radio button in the parameters section, and enter 2048 for the number of bits in the generated key.
    • Click on the ‘Generate’ button.
    • After generating the key, using the ‘Conversions’ menu and select ‘Export OpenSSH key’ and save the private key to a file, e.g. ‘id_rsa’.
Puttygen Window
Place the keys in the correct locations
  1. Move the private key (id_rsa as above) to a protected area on the source server, and remove all group and other access. Also, verify that this location and filename correspond to what you configured for the identityFile parameter in the relay_recipients.ini configuration file.
  2. Copy the public key to the mail gateway and append the contents to the authorized_keys file for the user you setup for copying files above. For example, if you called this user ‘postrelay’, the this file is something like /home/postrelay/.ssh/authorized_keys (your system may place user home directories in a slightly different location).
  3. Test with a simple scp command, e.g. scp -i /path/id_rsa localfile (the -i switch takes your private key file). If something fails, make sure SSH is properly configured on the mail gateway and that you’ve correctly added the public key to the authorize_keys file.
Load any needed perl modules

The script requires the following perl modules:

  1. Net::LDAP
  2. Config::IniFiles

If you do not already have these installed, using the the following command:

perl -MCPAN -e shell

Then install each module, e.g.:

cpan> install Net::LDAP

If you’ve not run this before, you will asked a series of questions before you can begin. It should be safe to accept the defaults and allow any prerequisites to be installed.

Execute script to test

Download the perl script and test it! Make sure you’ve given it execute privilege, e.g. chmod +x (add execute access for all users). If it fails in some way, the output should provide you with enough information to diagnose and fix your configuration error. If you’ve succeeded, you will have created a relay_recipient file locally and have it copied to the mail gateway. It should look like a series of key/value pairs with an e-mail address on the left side and ‘OK’ on the right side.

Configure relay recipient map on mail gateway

This is the final part of the Postfix configuration. We create and load the relay recipient map so that Postfix knows when to relay messages on to Exchange and when to reject messages altogether.

  1. Edit to add the proper parameter:
    relay_recipient_maps = "hash:/etc/postfix/relay_recipients"
  2. Build the map from the output of the perl script on the internal host that has been copied to the mail gateway
    postmap hash:/home/postrelay/relay_recipients
  3. If the command succeeds, copy both the text file and the new .db file to /etc/postfix
  4. Reload Postfix (postfix reload).
  5. Test by sending to a bogus address from an outside mail service. You should receive a bounce with…
    550 <>: Recipient address rejected: User unknown in relay recipient table

    If you would rather not show the unknown user table in the returned error message, then change the default behavior by adding the following to your file:

    show_user_unknown_table_name = no
Load and test shell script on mail gateway

Now we need a script to look for the new relay recipients file copied to the mail gateway, create a lookup table, copy the resulting file to the location Postfix expects to find it, and then reload Postfix itself. Note, there is a good reason for creating the lookup table before moving it into place. If we attempt to create the lookup table by overwriting the existing one and there is an error, we have no way to restore the previously in-place and working file! The script is shown below, but you download it here.


export PATH

# Check for file modified in the last 24 hours
RMAP=`find ~postrelay/relay_recipients -type f -mtime -1 -print`

if [ "$RMAP" != "" ]; then
    postmap hash:$RMAP
    if [ $STATUS -ne 0 ]; then
	exit 1
    cp ~postrelay/relay_recipients.db ~postrelay/relay_recipients /etc/postfix
    if [ $STATUS -ne 0 ]; then
	exit 1
    postfix reload
    if [ $STATUS -ne 0 ]; then
	exit 1

    echo "Successfully built and loaded new relay recipient hash map!\n"

exit 0

Modify the script as needed to match your environment. Move the script to a suitable location and remember to set the executable bit. Then test with a copied file in place to make sure it runs as you expect.

Schedule perl script and shell script execution

Now create crontab entries for the perl script on the internal host. Add the entry for a user that has write access to the location where the output file is to be written. This entry might look like:

30 0 * * * /usr/local/bin/

This will run the script at 00:30 (or 12:30am) every day. Often you will see the output of cron jobs sent to /dev/null by adding the following after the script:

>/dev/null 2>&1

This sends standard output and standard error to the null device. In this case, we do not want this behavior. The script will not have any output if nothing has changed from the previous run, otherwise the differences will be shown and this output will normally be mailed to the owner of the crontab.

The crontab entry for the shell script on the mail gateway must be for a user that has rights to write to the Postfix configuration directory and reload Postfix itself, e.g. root.

0 6 * * * /usr/local/etc/postfix/

This entry will cause the shell script to be run every morning at 6:00am. Again, if there is no output, there will be no mail sent, otherwise the crontab owner will receive the output by e-mail.

Concluding Remarks

So, all this seems like a bit of work, doesn’t it? How much does it really matter? Even in a small organization this prevented 60,000+ messages for invalid addresses from reaching the internal Exchange server each day! This can be the difference between an internal mail server that responds sluggishly or promptly. What’s worse is that Exchange 2007 enables tarpitting on receive connectors by default, so that the response to the mail gateway regarding the invalid recipient could be delayed, but this holds open connections and consumes resources on both ends. Tarpitting is effective at the edge of your network. The theory is that if you can delay a spammer even a few seconds, their throughput drops to the point where it becomes unprofitable for them to operate given their relatively low return rate, so many will just drop the connection if delayed, protecting you from extra spam and directory harvest attacks (but this is an evolving cat-and-mouse game).  However, having this technique deployed between your mail gateway and your internal Exchange server is counter-productive, so take control of your messaging environment and configure for security and efficiency!

Just as importantly as overall responsiveness, the above configuration shields your internal server from some types of e-mail denial-of-service attacks — yes, your externally-facing server is still vulnerable to bombardment of undeliverable mail (unless you use the first method described in the introduction), but internal users can continue to send and receive mail from each other.