Mailserver Installation with DKIM for your Webapp

By Loïc d'Anterroches for Céondo Ltd
Last updated Friday 6 January 2012.

Why Our Own Mail Server?

At the moment, the trend is to externalize the management of a company mail server in the cloud and pay a per email fee. It is good if you do not want to worry about the security implications of managing a mail server, but they are usually pretty expensive for something which can be very easily managed in house. It is only when you start to send really a lot of emails that you need to have a very robust infrastructure and in this case, your system has most likely already been growing with you.

Setting up your own mail server with SMTP authentication, DKIM signing and SPF records at the DNS level is relatively easy, this is why here at Céondo we have setup a dedicated mail server on our Ganeti cluster.

Requirements for the Mail Server

This mail server is setup to serve web applications to send and receive emails and email clients to send emails. No IMAP servers are provided, we use external providers to manage our mailboxes.

The server is running Debian Squeeze.

For the Web Applications

For the Remote Users

As we have a very limited number of clients, the login/password can be stored as a file on the mail server. No need to setup a database server for a maximum of 5 clients.

Setup of the Mail Server

The base server is a Debian Squeeze server running as a KVM virtual machine. It has two interfaces, one public on our RIPE block 178.33.145.149 and one private on the internal network 192.168.1.109.

Installation of the Packages

First some utility tools are installed after a full upgrade.

apt-get update && apt-get dist-upgrade
apt-get install sysstat screen curl rsync dialog wget unzip telnet

Then, the mail server related packages are installed.

apt-get install postfix

When the type of mail server is asked, Internet Site is selected and in our case mail.ceondo.net is used as fully qualified name.

For the signing of the outgoing email, OpenDKIM is used:

apt-get install opendkim

For the authenticated SMTP connection, SASL2 is used:

apt-get install libsasl2-modules sasl2-bin

Now, you can already test a connection to the mail server:

$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 vm109.ceondo.net ESMTP Postfix (Debian/GNU)
EHLO blah
250-vm109.ceondo.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
quit
221 2.0.0 Bye

So, the hostname is used for the hello and as we are using a different name than the effective hostname of the server, we need to change the banner and reload the server:

# postconf -e 'smtpd_banner = mail.ceondo.net ESMTP Postfix (Debian/GNU)'
# postconf -e 'myhostname = mail.ceondo.net'
# /etc/init.d/postfix reload
Reloading Postfix configuration...done.

Which provides us with the correct banner:

$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.ceondo.net ESMTP Postfix (Debian/GNU)
quit
221 2.0.0 Bye
Connection closed by foreign host.

Note that this setup is already allowing you to send emails from this server to other servers. Also, be sure that a reverse lookup of your mail server IP address is correcly providing the name in the banner. The reverse lookup is a standard control of the spam filters.

Configuration of the Access Restrictions

The internal networks an the authenticated users are able to send emails through the SMTP server, we reject the others.

postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject'

We also need to enable SASL:

postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_sasl_local_domain = mail.ceondo.net'
postconf -e 'broken_sasl_auth_clients = yes'

Of course do not forget to reload Postfix to test.

$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.ceondo.net ESMTP Postfix (Debian/GNU)
EHLO bloah
250-mail.ceondo.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-AUTH PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-AUTH=PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
quit
221 2.0.0 Bye

Here we can see that authentification is available. We can now open the server on the port 587 and try to send an email from our remote connection.

Configuration of the Extra Open Port

Add the line:

587       inet  n       -       n       -       -       smtpd

to the /etc/postfix/master.cf file and reload. You can now try to send an email from your desktop:

$ telnet mail.ceondo.net 587
Trying 178.33.145.149...
Connected to mail.ceondo.net.
Escape character is '^]'.
220 mail.ceondo.net ESMTP Postfix (Debian/GNU)
EHLO blah
250-mail.ceondo.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-AUTH PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-AUTH=PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
MAIL FROM:you@hostname.com
250 2.1.0 Ok
RCPT TO:somewhere@hostname.com
554 5.7.1 <somewhere@hostname.com>: Recipient address rejected: Access denied
quit
221 2.0.0 Bye
Connection closed by foreign host.

Ok, everything is in order, your server is not an open relay and will not accept a non authenticated connection. Time to setup the database with a login and a password to authenticate the client and allow the local network to send emails without authentication.

SMTP Authentication

The IP address based authentication is already at work when you send an email from the same host, it is defined by:

mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128

coupled with the permit_mynetworks in smtpd_recipient_restrictions. So, to add the 192.168.0.0/16 network, just run:

postconf -e 'mynetworks = 192.168.0.0/16 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128'

Now, you can open a connection to the mail server from another machine on the network and you will be able to send an email.

The last step is of course to setup the login/password authentication, this is done with the corresponding SASL tool:

# saslpasswd2 -c -u mail.ceondo.net -a smtpauth testuser
Password: 
Again (for verification): 

You can list the users in the database:

# sasldblistusers2 
testuser@mail.ceondo.net: userPassword

Ok, we can try to send an email now:

$ telnet mail.ceondo.net 587
Trying 178.33.145.149...
Connected to mail.ceondo.net.
Escape character is '^]'.
220 mail.ceondo.net ESMTP Postfix (Debian/GNU)
EHLO blah
250-mail.ceondo.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-AUTH PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-AUTH=PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
AUTH PLAIN AHRlc3R1c2VyLmNlb25kby5uZXQAcGFzc3dvcmRt
535 5.7.8 Error: authentication failed: authentication failure
quit
221 2.0.0 Bye
Connection closed by foreign host.

Not working... not so nice, but we are lucky, postfix has some logs:

# cat /var/log/mail.info | grep SASL
Aug 18 09:11:47 mail postfix/smtpd[12003]: warning: SASL authentication problem: unable to open Berkeley db /etc/sasldb2: Permission denied
Aug 18 09:11:47 mail postfix/smtpd[12003]: warning: SASL authentication failure: Password verification failed

So, let us check the rights:

# ls -la /etc/sasldb2 
-rw-rw---- 1 root sasl 12288 Aug 18 09:01 /etc/sasldb2

and change them for Postfix to be able to read the content and have the content of the database in its chrooted environment.

chown root:postfix /etc/sasldb2

Then you need to make the copy of the /etc/sasldb2 file in the chroot at each reload/restart (if you add new users, you will need to reload to have the update of the database file).

Find in /etc/init.d/postfix the line with:

FILES="etc/localtime etc/services etc/resolv.conf etc/hosts \
       etc/nsswitch.conf etc/nss_mdns.config"

and replace it with:

FILES="etc/localtime etc/services etc/resolv.conf etc/hosts \
       etc/nsswitch.conf etc/nss_mdns.config etc/sasldb2"

Now you can try from your laptop:

$ telnet mail.ceondo.net 587
Trying 178.33.145.149...
Connected to mail.ceondo.net.
Escape character is '^]'.
220 mail.ceondo.net ESMTP Postfix (Debian/GNU)
EHLO blah
250-mail.ceondo.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-AUTH PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-AUTH=PLAIN NTLM CRAM-MD5 LOGIN DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
AUTH PLAIN AHRlc3R1c2VyAHBhc3N3b3Jk
235 2.7.0 Authentication successful
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

It works and if you wonder how do you get the AHRlc3R1c2VyAHBhc3N3b3JkbQ== line, this is simple, just type in a terminal:

perl -MMIME::Base64 -e 'print encode_base64("\000testuser\000password")'

Testing the TLS Support

In all the answers from the SMTP daemon, 250-STARTTLS told us that TLS was enabled, we can test it:

openssl s_client -starttls smtp -crlf -connect mail.ceondo.net:587

you will see the TLS exchange and then be able to authenticate and send an email over the encrypted channel. Authentication, encryption, the only thing left is to be able sign the emails with DKIM.

DKIM Signing of the Emails

DKIM is using a private/public key system to allow the receiver of an email to very the integrity of the email. The public key used to verify the signature of the email is delivered by the DNS server of the sender email.

So, the verification flow is:

As it relies on a DNS lookup, it means that you, as the owner of example.org, need to add your public key in your DNS record of example.org. This also means that this system is as robust as the DNS system. That is, not very but good enough for this purpose.

So, for our mail server to correctly sign the outgoing emails and allow the receiver servers to check the signature we need:

  1. a public/private key pair to do the signing.
  2. control over our DNS server to kind of upload the public key.
  3. setup Postfix to correctly sign the outgoing emails with the private key of the right domain.

Generation of the Key Pair

For once, this is simple as OpenDKIM provides a utility to do it for us. You should do some tests with a non critical domain first, in my case, my personal domain:

# opendkim-genkey -d danterroches.org -s mail -t
# ls
mail.private  mail.txt

What we get is a private key in mail.private and what to put as TXT entry for your DNS record — that is, the public key with some added information. It is following the Bind format.

Setup of the DNS Server

We are going to setup directly both an SPF record and the DKIM public key. For a Bind zone, just add:

mail._domainkey 28800 IN TXT v=DKIM1; g=*; k=rsa; t=y; p=MIGfMA...
@ 10800 IN TXT v=spf1 a:mail.ceondo.net include:_mailcust.gandi.net ~all

The first line is the DKIM public key (truncated at the end) and the second one the SPF record. What is defined in the SPF record is that my provider Gandi is allowed to send emails too. Of course they will not be able to sign my emails with my DKIM private key, but this is why t=y is set for the key, it means that if not signed, the email should not be rejected (the server can possibly mark it as more susceptible to be some spam).

You can test to see if the DNS record is correctly set:

$ dig +short TXT danterroches.org.
"v=spf1 a:mail.ceondo.net include:_mailcust.gandi.net ~all"
$ dig +short TXT mail._domainkey.danterroches.org.
"v=DKIM1\; g=*\; k=rsa\; t=y\; p=MIGfMA0GCS..."

The mail part in mail._domainkey is the selector or the name of the generated key pair.

Time to Sign the Outgoing Emails

To sign, OpenDKIM must be configured, launched and Postfix must be configured to interact with the OpenDKIM milter. A simple configuration for a single domain is pretty simple, here are the 3 changed lines in /etc/opendkim.conf:

Domain      danterroches.org    
KeyFile     /etc/opendkim/mail.key
Selector    mail    
Socket      inet:8991@127.0.0.1

If you are going to sign emails coming from virtual machines on your local network, you should add your local network to the list of sources to sign.

InternalHosts       127.0.0.1,192.168.1.0/24

Of course you need to create the /etc/opendkim folder and copy the mail.private file (your private key) as mail.key in the folder. Chmod the key to 0600 and change the owner to opendkim:

chmod 0600 /etc/opendkim/mail.key
chown opendkim:opendkim /etc/opendkim/mail.key

and start the milter.

# /etc/init.d/opendkim start
Starting OpenDKIM: opendkim.

You can check that the milter is correctly listening for work:

# netstat -a | grep 8991
tcp    0    0 localhost:8991   *:*     LISTEN  

Now, the game is to have Postfix push the outgoing emails to the OpenDKIM milter before sending them.

In the /etc/postfix/main.cf file we need to add the milter:

smtpd_milters = inet:localhost:8991
non_smtpd_milters = inet:localhost:8991
milter_protocol = 2
milter_default_action = accept

The default action is important, it means that if the OpenDKIM milter fails, it will still send the email. Do not forget to restart Postfix.

Time to test.

$ openssl s_client -starttls smtp -crlf -connect mail.ceondo.net:587
EHLO blah
AUTH PLAIN auth-string
MAIL FROM:you@danterroches.or
rcpt to:you@danterroches.org
DATA
From: me@danterroches.org
To: me@danterroches.org
Subject: Testing the milter

Hello,

I am testing the milter of Postfix.

loic

.
250 2.0.0 Ok: queued as F14AD1FD75
quit

And now, waiting a bit and checking the email, I find the headers:

DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=danterroches.org;
    s=mail; t=1313746486;
    bh=GlQYLi2MdsEJ1lCtPhoJ6j2+PvMu2G/vEoDmWwqlWqo=; h=From:To:Subject;
    b=JrkasVKpQwhanu1Uz9hO7cKy8NPo/bzs7tkVxu6xOMBCKJJDn/jRmnDiaQt5JOSAT
     a0r32KJyY2K+GrQfkNRB2Ucq0/jsPl15FVZQVTooi2o0eLhecBHawxvYSUcgkH3pj8
     xmtu32pDuAVNRTnzwphbxK+EoBdCicu2ECsEYdbg=

The email has been signed successfully. Happy day!

A Bit Further with Multiple Domains

If you are well securing your email server, you can simply add the same public key and SPF records for all the domains and in the /etc/opendkim.conf file, simply change the line:

Domain      danterroches.org    

with:

Domain      danterroches.org, example.org, example.net

You can also use a database, a file, a sleepycat database. man opendkim will give you the way to configure a dataset. OpenDKIM is very flexible digging into the documentation will provide you with a large list of options and functionalities to match your requirements.

Receiving Emails for the Webapp

If you rationalize a bit the way you work, your mail server will handle many web applications and as such will need to receive emails for many domains. This is handled very easily with the virtual_alias_domains system of Postfix.

For example we are going to handle the domain [gitmanual.org](http://gitmanual.org] with this system. So, first, we inform that we want to handle this domain as a virtual domain:

postconf -e 'virtual_alias_domains = gitmanual.org'

Be sure to never add a domain both in the mydestination and the virtual_alias_domains entries.

We need also to define what to do with the emails coming for this domain, we define for this an alias map:

postconf -e 'virtual_alias_maps = hash:/etc/postfix/virtual'

The content of the file is for example:

foo@gitmanual.org   gitmanual
bar@gitmanual.org   gitmanual

This means that we are going to accept only emails for foo and bar, and send them to the local alias gitmanual. Now, we need to define this alias.

Open /etc/aliases and add at the end:

gitmanual:     you@yourdomain.com

This means that the emails coming for foo and bar on the gitmanual.org domain will be forwarded to you@yourdomain.com. You cannot really make it simpler.

You need now to compile the virtual and aliases databases:

# postmap /etc/postfix/virtual
# newaliases

If you try now to send an email, it will fail because your mail server is still secured not to receive emails from smtp servers or client outside of mynetworks or SASL authenticated. So, we need to add the reception of emails for the authorized domains in the virtual alias (and as a side effect to the mydestination domains).

postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, \
   permit_auth_destination, permit_sasl_authenticated, reject'

Note the addition of permit_auth_destination compared to the previous configuration. Do not forget to reload the configuration:

# /etc/init.d/postfix reload

Receiving Emails and Handling Them With a Script

You first need to be able to receive emails, then you simply create an alias which is piping the content to a command:

gitmanual:    "|/usr/bin/php -q /path/to/script.php gitmanual"

Here you can for example read the content of the email and process it:

<?php
$email = file_get_contents('php://stdin');
// do something with $email
exit($errorcode);

The $errorcode should be:

Spam Filtering

If you receive emails for your webapplication, you may want to verify the DKIM signature, greylist and spam filter.

Conclusion

At the end, the setup is simple, robust and pretty fast to setup. It requires only a sound understanding of the elements (DNS, SMTP) in play. If you are not sending 100,000's emails in a day, you do not need more. The verification of the DKIM signature is not shown here as not really needed.

Changelog

We are looking for αlpha and βeta testers for a new Git hosting, interested? Drop me an email at loic@ceondo.com.
We are especially interested with companies/people located in Europe.

All the notes are copyright © 2011-2012 Céondo Ltd, all rights reserved.