Ubuntu: DMZ Mail Relay
From ReceptiveIT
Contents |
Abstract
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?
Connections
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?
Firewall
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 OutPutFile.Close Set FileSystem = Nothing Set Container = Nothing 'Say Finished when your done WScript.Echo "Finished" WScript.Quit(0) '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 Next End If Case "organizationalunit" , "container" EnumerateUsers User End Select Next End Sub
Check output
Run the script and check the output. It should give you something like this:
alias: SMTP:newuser3@office.example.com alias: SMTP:newuser@office.example.com alias: SMTP:Administrator@office.example.com alias: X400:c=us;a= ;p=Example Organiza;o=Exchange;s=Administrator; alias: smtp:postmaster@office.example.com alias: X400:c=us;a= ;p=Example Organiza;o=Exchange;s=Doe;g=Jon; alias: SMTP:testuser@example.com alias: SMTP:postfix@office.example.com 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
Makefile
Download Makefile
DB=db all: relay_recipients.$(DB)
- "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
- We need virtual.txt to build relay_recipients.proto
- awk will use ":" as field separator and for each line
- that contains "alias: (SMTP|smtp):" it will do:
- print the third row, insert a TAB, insert "OK" and add a newline
- into relay_recipients.proto
%.$(DB): %.proto
/usr/sbin/postmap $*.proto && mv $*.proto.$(DB) $*.$(DB)
- Building a *.db requires a *.proto file. If that exists,
- postmap is called to build the map from *.proto. If postmap is successful
- 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 "user@example.com 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
$dc1="server.domain.local";
$dc2="server2.domain.local";
# 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"
$hqbase="ou=MyBusiness,dc=domain,dc=local";
# 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"
$user="cn=ldap,ou=MyBusiness,dc=domain,dc=local";
$passwd="password";
# Connecting to Active Directory domain controllers
$noldapserver=0;
$ldap = Net::LDAP->new($dc1) or
$noldapserver=1;
if ($noldapserver == 1) {
$ldap = Net::LDAP->new($dc2) or
die "Error connecting to specified domain controllers $@ \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)
(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact))
(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
$page->cookie($cookie);
}
if ($cookie) {
# We had an abnormal exit, so let the server know we do not want any more
$page->cookie($cookie);
$page->size(0);
$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:
/etc/postfix/main.cf
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.
/etc/postfix/transport
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
# 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:
# 1.3.6.1.4.1.36958
# 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 <alex@receptiveit.com.au>
# Receptive IT Solutions
# PO Box 643
# Goulburn NSW 2580
# Australia
#
#
# Attributes start here
#
attributetype ( 1.3.6.1.4.1.36958.1.1.1.1
NAME 'ritImapMailAccess'
SINGLE-VALUE
EQUALITY booleanMatch
DESC 'Allow/Disallow access flag for IMAP'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
attributetype ( 1.3.6.1.4.1.36958.1.1.1.2
NAME 'ritSmtpMailAccess'
SINGLE-VALUE
EQUALITY booleanMatch
DESC 'Allow/Disallow access flag for SMTP'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
attributetype ( 1.3.6.1.4.1.36958.1.1.1.3
NAME 'ritPopMailAccess'
SINGLE-VALUE
EQUALITY booleanMatch
DESC 'Allow/Disallow access flag for POP3'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
attributetype ( 1.3.6.1.4.1.36958.1.1.1.4
NAME 'ritWebMailAccess'
SINGLE-VALUE
EQUALITY booleanMatch
DESC 'Allow/Disallow access flag for HTTP/HTTPS'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
attributetype ( 1.3.6.1.4.1.36958.1.1.1.5
NAME 'ritWebAccess'
SINGLE-VALUE
EQUALITY booleanMatch
DESC 'Allow/Disallow access flag for HTTP/HTTPS'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
attributetype (1.3.6.1.4.1.36958.1.1.1.11
NAME 'ritProxyHost'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
DESC 'Fully qualified hostname of a proxyserver' )
attributetype (1.3.6.1.4.1.36958.1.1.1.10
NAME 'ritMailHost'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
DESC 'Fully qualified hostname of a mailserver' )
#
# Objects start here
#
objectclass ( 1.3.6.1.4.1.36958.1.1.2.1
NAME 'ritMailAccount'
SUP top
AUXILIARY
DESC 'Acount for accessing mail services'
MUST (uid $ userPassword )
MAY ( ritImapMailAccess $ ritPopMailAccess $ ritSmtpMailAccess $ ritWebMailAccess $ ritMailHost ) )
objectClass ( 1.3.6.1.4.1.36958.1.1.2.2
NAME 'ritProxyAccount'
SUP top
AUXILIARY
DESC 'Account for accessing the proxy server'
MUST (uid $ userPassword )
MAY ( ritProxyHost $ ritWebAccess) )
# EOF
DMZ Services
SMTP
/etc/saslauthd.conf
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
IMAP
/etc/dovecot/dovecot.conf
passdb ldap {
# Path for LDAP configuration file
args = /etc/dovecot/dovecot-ldap.conf
}
userdb ldap {
# Path for LDAP configuration file
args = /etc/dovecot/dovecot-ldap-userdb.conf
}
/etc/dovecot/dovecot-ldap.conf
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
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.
Webmail
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 192.168.0.0/24 Satisfy any

