Ubuntu: DMZ Mail Relay

From ReceptiveIT
Jump to: navigation, search


The Microsoft Exchange server is, without any doubt, a very powerful groupware server. It's security record and it's stability, when it comes to attacks, are not that famous. That's why many postmasters want to combine the groupware functionality with the security and stability of UNIX smtp gateways.

This is a little sketch to give you an idea how this can be done, using Postfix - an alternative to the widely-used Sendmail program.

What should be protected?


First and foremost the idea is to keep the Exchange Server from connecting directly to other servers and vice versa have other clients that come from the Internet connect directly to the Exchange server. This can easily be done by following man transport (5). You need to configure Postfix to accept mails and transport them to the final destination - your Exchange server.

Performance aka recipient lookups

In our scenario the Exchange server is the heart of our communication infrastructure and we want to keep it beating constantly without any disturbance. Part of running a service smoothly is making sure that anything that could disturb it is blocked as soon as possible.

If you run Postfix as a simple mailrelay it will have no notion of the users the Exchange Server holds. This means that by default Postfix will accept messages for its destination and transport those to the Exchange server. The Exchange server will accept the message, and notice later that the recipient doesn't even exist. In that case the mail will be bounced.

If it wasn't for SPAM this would be an OK approach. But SPAM is all over the place and some spammers try to get their message delivered using dictionary attacks. A dictionary attack equals a brute force attack. To withstand brute force you must be able to decide what's right and what's wrong very fast and you need lots of performance, stability (and a few nasty tricks).

Note In a dictionary attack the attacker tries any username/alias it can think of in combination with the domainname the SMTP server is registered for, in order to get messages delivered to any user known by the mailserver. Thousands of messages are sent to the final destination every minute, challenging the mailserver to examine every message and make a decision on the recipients validity.

If the machine is configured to bounce mail to unknown local users, it will run into trouble due to the large number of messages that need to be bounced.

If the machine is configured to deliver mail destined for unknown user to e.g. postmaster, then postmaster's mailbox will be filled until the disk is full.

MAPS built from LDAP queries

The right thing™ to do, is to build maps that were built from files who were built from LDAP queries run against the Exchanges' ADS. Huh?

Warning Probably the first impulse will be to have Postfix do LDAP lookups on demand by connecting to the Exchange Server through LDAP and query for valid recipients. This is not recommended, because in critical situations e.g. during a dictionary attack there will be thousands of queries a minute, distracting the Exchange server from its primary job being a groupware server. There will be so many queries that the Exchange server will slow down so badly that the dictionary attack will result in a DoS to your groupware server.

The Exchange server can be queried for valid recipients using LDAP. However in a big Exchange Server installation there is a default limit that might permit a query from getting all valid recipients; a regular query is allowed to return at most 1.000 results. After that the server will not answer and further requests by the source that seeks recipients.

The solution is to use Microsoft's own scripting language and write a Visual Basic script that will tell the LDAP server not to give all results at once, but to hand them out in pages. The following steps sketch a way how to query an Exchange Server for valid recipients, copy them to the Postfix mailrelay and have Postfix use them to reject unknown recipients before they reach the Exchange Server.

How to protect the server?


Put the Exchange server into your LAN and keep it hidden from the internet by a firewall. All you need to allow is:

Incoming connections

Allow incoming connections on Port 25 from your mailrelay to the exchange server

Outgoing connections

Allow outgoing connections on Port 25 from your exchange server to the mailrelay only

Allow outgoing connections from on Port 22 from your exchange server to the mailrelay

Exchange Server

Export LDAP query to file The idea is to have a script query an Exchange server for proxyAddresses which is the correct attribute when you are looking for valid recipients.

Edit the script and change Set Container=GetObject("LDAP://CN=Users,DC=office,DC=example,DC=com") to fit your LDAP structure.

Example 1. export_recipients.vbs

Download export_recipients.vbs.

' Export all valid recipients (= proxyAddresses) into a
' file virtual.txt
' Ferdinand Hoffmann & Patrick Koetter
' 20021100901
' Shamelessly stolen from 
' http://www.microsoft.com/windows2000/techinfo/ \
' planning/activedirectory/bulksteps.asp

'Global variables
Dim Container
Dim OutPutFile
Dim FileSystem

'Initialize global variables
Set FileSystem = WScript.CreateObject("Scripting.FileSystemObject")
Set OutPutFile = FileSystem.CreateTextFile("virtual.txt", True)
Set Container=GetObject("LDAP://CN=Users,DC=office,DC=example,DC=com")

'Enumerate Container
EnumerateUsers Container 

'Clean up
Set FileSystem = Nothing
Set Container = Nothing

'Say Finished when your done
WScript.Echo "Finished"

'List all Users
Sub EnumerateUsers(Cont)
Dim User

'Go through all Users and select them
For Each User In Cont
Select Case LCase(User.Class)

'If you find Users
Case "user"
  'Select all proxyAddresses
  Dim Alias
  If Not IsEmpty(User.proxyAddresses) Then
    For Each Alias in User.proxyAddresses
    OutPutFile.WriteLine "alias: " & Alias
    'WScript.Echo Alias
  End If

Case "organizationalunit" , "container"
  EnumerateUsers User

End Select
End Sub

Check output

Run the script and check the output. It should give you something like this:

alias: SMTP:[email protected] 
alias: SMTP:[email protected]
alias: SMTP:[email protected]
alias: X400:c=us;a= ;p=Example Organiza;o=Exchange;s=Administrator;
alias: smtp:[email protected] 
alias: X400:c=us;a= ;p=Example Organiza;o=Exchange;s=Doe;g=Jon;
alias: SMTP:[email protected]
alias: SMTP:[email protected]
alias: X400:c=us;a= ;p=Example Organiza;o=Exchange;s=postfix;

SMTP is a valid recipient and it is a users mail address that any outgoing message will be rewritten to smtp as an alias and a valid recipient for a user.

Copy file to mailrelay

Install and configure any secure copy mechanism, e.g. PuTTY to scp virtual.txt to the Postfix machine using key files for unattended copying. You can also use rsync over ssh from the cygwin package.

Use the Windows Scheduler to run export_recipients.vbs and scp as often as needed.

Build proto MAP Parse the virtual.txt file for smtp and SMTP entries and write them into a Postfix map format to relay_recipients.proto. Then run postmap /path/to/relay_recipients.proto to make it a Postfix readable DB. We use this Makefile which can be invoked from cron using: cd /etc/postfix && make


Download Makefile

DB=db all: relay_recipients.$(DB)

  1. "all" means to build virtual.db

relay_recipients.proto: virtual.txt

   awk -F: '/alias: (SMTP|smtp):/ {printf("%s\tOK\n",$$3)}' virtual.txt > relay_recipients.proto
  1. We need virtual.txt to build relay_recipients.proto
  2. awk will use ":" as field separator and for each line
  3. that contains "alias: (SMTP|smtp):" it will do:
  4. print the third row, insert a TAB, insert "OK" and add a newline
  5. into relay_recipients.proto

%.$(DB):  %.proto

   /usr/sbin/postmap $*.proto && mv $*.proto.$(DB) $*.$(DB)
  1. Building a *.db requires a *.proto file. If that exists,
  2. postmap is called to build the map from *.proto. If postmap is successful
  3. the *.proto map will be renamed to *.db

Alternative - Generate list from mail relay

Big fat warning - This method requires that you allow some level of access from the mail relay to the Windows Server. While you might prefer to use a Linux environment to create the list, it is probably a good idea to tighten security. That said, here is a perl script that will do the job.

#!/usr/bin/perl -T -w

# Version 1.02

# This script will pull all users' SMTP addresses from your Active Directory
# (including primary and secondary email addresses) and list them in the
# format "[email protected] OK" which Postfix uses with relay_recipient_maps.
# Be sure to double-check the path to perl above.

# This requires Net::LDAP to be installed.  To install Net::LDAP, at a shell
# type "perl -MCPAN -e shell" and then "install Net::LDAP"

use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );

# Enter the path/file for the output
$VALID = "/etc/postfix/exchange_recipients";

# Enter the FQDN of your Active Directory domain controllers below

# Enter the LDAP container for your userbase.
# The syntax is CN=Users,dc=example,dc=com
# This can be found by installing the Windows 2000 Support Tools
# then running ADSI Edit.
# In ADSI Edit, expand the "Domain NC [domaincontroller1.example.com]" &
# you will see, for example, DC=example,DC=com (this is your base).
# The Users Container will be specified in the right pane as
# CN=Users depending on your schema (this is your container).
# You can double-check this by clicking "Properties" of your user
# folder in ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=Users,DC=example,DC=com
# which would be $hqbase="cn=Users,dc=example,dc=com"
# Note:  You can also use just $hqbase="dc=example,dc=com"

# Enter the username & password for a valid user in your Active Directory
# with username in the form cn=username,cn=Users,dc=example,dc=com
# Make sure the user's password does not expire.  Note that this user
# does not require any special privileges.
# You can double-check this by clicking "Properties" of your user in
# ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=user,CN=Users,DC=example,DC=com
# which would be $user="cn=user,cn=Users,dc=example,dc=com"
# Note: You can also use the UPN login: "user\@example.com"

# Connecting to Active Directory domain controllers
$ldap = Net::LDAP->new($dc1) or
if ($noldapserver == 1)  {
   $ldap = Net::LDAP->new($dc2) or
      die "Error connecting to specified domain controllers [email protected] \n";

$mesg = $ldap->bind ( dn => $user,
                     password =>$passwd);
if ( $mesg->code()) {
    die ("error:", $mesg->code(),"\n","error name: ",$mesg->error_name(),
        "\n", "error text: ",$mesg->error_text(),"\n");

# How many LDAP query results to grab for each paged round
# Set to under 1000 for Active Directory
$page = Net::LDAP::Control::Paged->new( size => 990 );

@args = ( base     => $hqbase,
# Play around with this to grab objects such as Contacts, Public Folders, etc.
# A minimal filter for just users with email would be:
# filter => "(&(sAMAccountName=*)(mail=*))"
         filter => "(& (mailnickname=*) (| (&(objectCategory=person)
                    (objectCategory=group)(objectCategory=publicFolder) ))",
          control  => [ $page ],
          attrs  => "proxyAddresses",
my $cookie;
while(1) {
  # Perform search
  my $mesg = $ldap->search( @args );

# Filtering results for proxyAddresses attributes
  foreach my $entry ( $mesg->entries ) {
    my $name = $entry->get_value( "cn" );
    # LDAP Attributes are multi-valued, so we have to print each one.
    foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
     # Test if the Line starts with one of the following lines:
     # proxyAddresses: [smtp|SMTP]:
     # and also discard this starting string, so that $mail is only the
     # address without any other characters...
     if ( $mail =~ s/^(smtp|SMTP)://gs ) {
       push(@valid, $mail." OK\n");

  # Only continue on LDAP_SUCCESS
  $mesg->code and last;
  # Get cookie from paged control
  my($resp)  = $mesg->control( LDAP_CONTROL_PAGED ) or last;
  $cookie    = $resp->cookie or last;

  # Set cookie in paged control

if ($cookie) {
  # We had an abnormal exit, so let the server know we do not want any more
  $ldap->search( @args );
  # Also would be a good idea to die unhappily and inform OP at this point
     die("LDAP query unsuccessful");
# Only write the file once the query is successful
open VALID, ">$VALID" or die "CANNOT OPEN $VALID $!";
print VALID @valid;
# Add additional restrictions, users, etc. to the output file below.
#print VALID "user\@example.com OK\n";
#print VALID "user1\@example.com 550 User unknown.\n";
#print VALID "bad.example.com 550 User does not exist.\n";

close VALID;

Configure Postfix

Once the makefile has built the recipient map we need to configure Postfix to make use of the map. We must also tell Postfix where to transport messages to as soon as they have passed the recipient test. In /etc/postfix/main.cf we add these parameters:


relay_domains = office.example.com, example.com
relay_recipient_maps = hash:/etc/postfix/relay_recipients
transport_maps = hash:/etc/postfix/transport

The parameter transport_maps in main.cf points to the location of the transport map. The map contains entries that tell Postfix how and where to transport messages for a given recipient domain to.


example.com           smtp:[ho.st.na.me] 
office.example.com    smtp:[ip.ad.dr.es] 

Hostnames must be fully quallified domain names resolvable by Postfix.

Note You may not be able to configure your DNS server to provide a separate MX entry for the host that messages should be transported to. In that case you simply put the hostname in square brackets and Postfix will not use the default MX a DNS-query would return.

Use an IP-Address, if Postfix cannot not resolve the hostname of the exchange server. Note that you must put the IP-Adress in square brackets.

Commit changes

Finally make Postfix reload it's configuration and it's maps by executing postfix reload.

/etc/init.d/postfix restart

LDAP schema


# receptiveit.schema
#                    ***********************************
#                    * USE THIS FILE AT YOUR OWN RISK! *
#                    ***********************************
# OID Guidelines
# - Every OID in this file utilises the following template: ns.a.b.c.d
#    ns - the official namespace Receptive IT:
#    a  - Partition, identifies the type of the OID 
#         0  : experimental,
#         1  : stable,
#         x  : reserved
#    b  - Reserved, must always be 1.     
#    c  - Entry type (1:attribute, 2:object)
#    d  - Serial number (increased with every new entry)
# Schema dependencies
#  core.schema
#  cosine.schema
# Contact information:
# Alex Ferrara <[email protected]>
# Receptive IT Solutions
# PO Box 643
# Goulburn NSW 2580
# Australia

# Attributes start here

attributetype (
        NAME 'ritImapMailAccess'
        EQUALITY booleanMatch
        DESC 'Allow/Disallow access flag for IMAP'
        SYNTAX )

attributetype (
        NAME 'ritSmtpMailAccess'
        EQUALITY booleanMatch
        DESC 'Allow/Disallow access flag for SMTP'
        SYNTAX )

attributetype (
        NAME 'ritPopMailAccess'
        EQUALITY booleanMatch
        DESC 'Allow/Disallow access flag for POP3'
        SYNTAX )

attributetype (
        NAME 'ritWebMailAccess'
        EQUALITY booleanMatch
        DESC 'Allow/Disallow access flag for HTTP/HTTPS'
        SYNTAX )

attributetype (
        NAME 'ritWebAccess'
        EQUALITY booleanMatch
        DESC 'Allow/Disallow access flag for HTTP/HTTPS'
        SYNTAX )

attributetype (
        NAME 'ritProxyHost'
        EQUALITY caseIgnoreIA5Match
        DESC 'Fully qualified hostname of a proxyserver' )

attributetype (
        NAME 'ritMailHost'
        EQUALITY caseIgnoreIA5Match
        SUBSTR caseIgnoreIA5SubstringsMatch
        DESC 'Fully qualified hostname of a mailserver' )

# Objects start here

objectclass (
        NAME 'ritMailAccount'
        SUP top
        DESC 'Acount for accessing mail services'
        MUST (uid $ userPassword )
        MAY ( ritImapMailAccess $ ritPopMailAccess $ ritSmtpMailAccess $ ritWebMailAccess $ ritMailHost ) )

objectClass (
        NAME 'ritProxyAccount'
        SUP top
        DESC 'Account for accessing the proxy server'
        MUST (uid $ userPassword )
        MAY ( ritProxyHost $ ritWebAccess) )


DMZ Services



ldap_auth_method: fastbind
ldap_servers: ldap://ldap.server.local/
ldap_version: 3
ldap_timeout: 10
ldap_time_limit: 10
ldap_scope: sub
ldap_search_base: ou=users,dc=domain,dc=local
ldap_filter: (&(uid=%U)(ritSmtpMailAccess=TRUE))
ldap_version: 3


This config is for Dovecot 2.0 playing the part of IMAP proxy using LDAP authentication. First we need to install Dovecot IMAP with LDAP support

apt-get install dovecot-imapd dovecot-ldap


uris = ldap://ldap.server.local
auth_bind = yes
pass_attrs = uid=user,mailHost=host,ritImapMailAccess=proxy
pass_filter = (&(uid=%u)(ritImapMailAccess=TRUE))
base = ou=users,dc=domain,dc=local


auth_mechanisms = plain
!include auth-ldap.conf.ext

An important performance tip is that dovecot-ldap-userdb.conf should be a symlink to dovecot-ldap.conf. This allows password and user lookups to happen on separate channels, and not be blockers for each other.


Apache conf

AllowOverride none
AuthType Basic
AuthzLDAPAuthoritative on
AuthName "Squirrelmail"
AuthBasicProvider ldap
AuthLDAPURL ldap://ldap.server.local/dc=domain,dc=local?uid
Require ldap-attribute ritWebMailAccess=TRUE
Order Allow,Deny
Allow from
Satisfy any