UPDATED: A definitive mail server guide

A successful telnet into a mail server

Recently, my colocated server crashed on me. This was a pretty good opportunity for me to move to the cloud. When I migrated up, part of the process was building a new CentOS server from the ground up. I’ll discuss the intricacies of the rest of the current setup later – as well as provide some handy backup scripts to copy my new server’s data to Amazon S3 – but certainly my biggest challenge was building a mail server from scratch using Postfix and Dovecot, as I hadn’t done anything like that before.

The good news is that there were a lot of tutorials, because this was something I hadn’t done before. The bad news…well, none of them were particularly great. So, after getting my mail server up and running and testing the hell out of it, I decided it was time to get a good tutorial up on the Internet somewhere, using what I’d learned from the extensive Postfix documentation (bookmark this, you won’t regret it), the Dovecot documentation (“Why Does It Not Work?” is a fantastic starting point), a couple incomplete tutorials, and a bit of trial and error.

In this post, we’re going to cover the complete setup of a functional mail server, from base CentOS install all the way up to virtual domains, users, and hosts. We’ll make sure an SSL gets installed so that your mail’s secure, and we’ll walk through testing the mail server afterwards. Due to the long config blocks, I opted not to create an Asciinema video for this one, but rest assured that I’ll do those when it’s appropriate in the future.

What we’ll do

Things that I will cover include:

  • Setting up a Postfix/Dovecot mail server with shared auth
  • Making sure your server only accepts secure connections
  • Virtual domains, users, and aliases
  • Testing your mail server using telnet
  • Verifying email using SPF and DKIM records

Things that I will not cover include:

  • Setting up your favorite web-based mail client. I believe that’s
    • outside of the realm of this tutorial
    • fairly self-explanatory anyway
  • Setting up a server for one domain and one user.
    • You can still run only one user/domain with virtual users and domains
    • It’s way easier to scale when you inevitably buy that second domain if everything’s already in place
    • Every tutorial I found that adds virtual users and domains after the fact is awful, just awful
  • Actually using Dovecot’s conf.d includes
    • The idea of using separate config files for separate config blocks in Dovecot is great, but the config is so darn short it’s a lot simpler to just stick things in one file. I’ll cover this in more depth later.
  • Advanced spam management
    • I will set up basic Spamhaus DNSBL checking, but anything further should be up to the individual user’s preference.

Things You Need

  • A domain – I use Hover for my domain services and recommend them without reservation. I’m using mydomain.com for the example configuration below.
  • A server – If this was 6 months ago I’d have told you to use DigitalOcean, but AWS now offers a similar-cost option at the low end – a t2.nano – and a whole slew of other services which I’ll be posting about taking advantage of in the future. I’d recommend spinning up a server there.
  • An IP – Make sure you’ve got whatever IP you’re assigning your webserver handy. We’ll need it later.

Getting Started

Go ahead and install CentOS on your server. The most recent installers give you a few more options than they used to. Notably, if you end up picking the wrong security profile you will probably have to install firewalld after first system boot. Just be mindful of that and make sure you’ve got everything set up appropriately. If you’re building from the minimal AWS AMI, everything should be configured fine initially and you won’t have to worry about it.

Required Packages

We’ll need a few things for everything to run smoothly. Run the following commands to make sure you have everything you need in place:

yum install epel-release -y
yum install postfix dovecot opendkim certbot -y

If, for whatever reason, you don’t like building minimal installs with CentOS, make sure you remove sendmail as well.

Laying the Groundwork

Before we get started with the meat of the configuration, we need to set up our virtual mail user. I used gid and uid 5000; you can use whatever you want, as long as it’s not in use by something else and as long as it’s over 1000. We’re also going to make a small change to the Postfix system user to add it to the opendkim group – this will come up later.

groupadd vmail -g 5000
useradd vmail -r -g 5000 -u 5000 -d /var/vmail -m -c "Virtual mail user" -s /sbin/nologin
usermod -a -G opendkim postfix

Next, we need to set up a server certificate. For this tutorial, we’re using the Certbot utility written to work with the free Let’s Encrypt service. If you have a web server running, shut it off for this next step – we don’t have a webroot, so certbot needs to set up a temporary web server to verify the domain. Run certbot certonly, follow the prompts, and generate your cert. I prefer to use mail.mydomain.com for the mail server cert. You’ll notice that the cert has been put in /etc/letsencrypt/live/mail.mydomain.com/fullchain.pem and the key is located in /etc/letsencrypt/live/mail.mydomain.com/privkey.pem.

Finally, make sure your firewall’s configured properly, opening ports using firewall-cmd --add-port XXX --zone=public --permanent. If on AWS, you can use a security group instead. Either way, make sure ports 993, 995, 25, 465, and 587 are open.

Postfix Setup

We’ll use Postfix as our SMTP server to actually send mail. It authenticates against Dovecot’s user database, which I’ll walk through setting up below.


main.cf is the heart of the Postfix configuration. Go ahead and hop in – make a backup of /etc/postfix/main.cf, then open it with your favorite editor, remove the contents of the file, and add the following (commented to provide additional information) lines:

queue_directory = /var/spool/postfix #Mail queued for delivery
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix
mail_owner = postfix
unknown_local_recipient_reject_code = 550
alias_maps = hash:/etc/postfix/aliases
alias_database = $alias_maps
myhostname = mail.mydomain.com
mydomain = mydomain.com
inet_interfaces = all #Listens on all IPs - it doesn't have to do this, but I find it an awful lot easier to, especially since we're using virtual domains and can run everything on one mail server.
inet_protocols = ipv4 #If you want to run this on IPv6, you'll need to set up your networking stack to allow for it, and change this to "ipv4, ipv6". There are a lot of tricky things associated with Postfix on IPv6, check http://www.postfix.org/IPV6_README.html for details.
mydestination = $myhostname, localhost.$mydomain, localhost

debug_peer_level = 2
debugger_command =
 ddd $daemon_directory/$process_name $process_id & sleep 5

sendmail_path = /usr/sbin/sendmail.postfix
newaliases_path = /usr/bin/newaliases.postfix
mailq_path = /usr/bin/mailq.postfix
setgid_group = postdrop
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /usr/share/doc/postfix-2.6.6/samples
readme_directory = /usr/share/doc/postfix-2.6.6/README_FILES

relay_domains = $mydestination

#These are the virtual aliases, domains, and mailboxes. I'll explain the formatting of the files below.

virtual_mailbox_base = /var/vmail #The home directory of the vmail user we set up above. This is where the mail gets stored.
virtual_minimum_uid = 5000 #Uid of vmail user
virtual_transport = virtual
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000

smtpd_sasl_auth_enable = yes #This is super important; we will only allow authenticated mail below. 
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/run/dovecot/auth-client
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
smtpd_sasl_local_domain = $mydomain
broken_sasl_auth_clients = yes

smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_rbl_client zen.spamhaus.org #Exclude authed and local clients from spam checks, check all against spamhaus.org
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination #Authed clients can specify any destination domain.

smtpd_use_tls = yes #Super important. We want to only allow secure connections.
smtpd_tls_key_file = /etc/letsencrypt/live/mail.mydomain.com/privkey.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.mydomain.com/fullchain.pem

smtpd_tls_loglevel = 3
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom


We need to take a quick detour into /etc/postfix/master.cf to make sure that Postfix is set up to use SASL auth with Dovecot. Open up the file, and uncomment lines starting with submission and smtps. Modify the -o lines as below, leaving you with a config block looking like this:

submission inet  n    -    n    -    -    smtpd
 -o smtpd_tls_security_level=encrypt
 -o smtpd_sasl_auth_enable=yes
 -o smtpd_client_restrictions=permit_sasl_authenticated,reject
 -o milter_macro_daemon_name=ORIGINATING
smtps    inet  n    -    n    -    -    smtpd
 -o smtpd_tls_wrappermode=yes
 -o smtpd_sasl_auth_enable=yes
 -o smtpd_client_restrictions=permit_sasl_authenticated,reject
 -o milter_macro_daemon_name=ORIGINATING

In addition, add another block, right above the submission block:

headers-cleanup unix n - - - 0 cleanup
 -o syslog_name=postfix/headers-cleanup
 -o header_checks=regexp:/etc/postfix/header_checks

I’ll go over what this is for in the next section.

Adding header checks

One thing that we need to set up before moving forward is header checks. This is something we can use to strip out some identifying headers from mail going out from our server. This is important because some of these headers can expose IP addresses, scripts on your server, mail clients, or other information. Here’s what I recommend you put in /etc/postfix/header_checks:

/^Message-ID:/i IGNORE
/^Mime-Version: 1.0.*/ REPLACE Mime-Version: 1.0
/^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE
/^X-Mailer:/ IGNORE
/^X-Originating-IP:/ IGNORE
/^X-PHP-Originating-Script:/ IGNORE

A lot of guides out there mention adding the “Received:” header to these checks. My problem with that is, as far as I know, RFC 1123 states that you shouldn’t modify Received: headers. You may decide that’s worth changing, but it’s something I decided to simply mention rather than recommend here.

Creating virtual domains, users, and aliases

Describing these as “virtual” makes some sense in that Postfix abstracts them out of the Unix authentication layer, but it’s still a little weird – when you log in, your username will be “me@mydomain.com”, with a unique password. Just accept the terminology and move on. With that, let’s dive in.

Virtual domains are our way of managing all of the domains that Postfix will be willing to manage. It’s pretty easy to set up – create /etc/postfix/vmail_domains with the following text:

mydomain.com     OK
myotherdomain.org     OK
mythirddomain.biz     OK

…and so on and so forth, until you’ve added all the domains you want to handle on this server. The “OK” text is kind of a placeholder – you could use “1”, or honestly whatever you want. If you remove it, that domain will be disabled, though.

Virtual mailboxes are where the fun really begins. This is how we set up all of the different accounts that we’re actually going to host on the server. Again, it’s pretty straightforward, but there are a couple of “gotchas” which I’ll cover. Create /etc/postfix/vmail_mailbox with the following text:

me@mydomain.com     mydomain.com/me/
second-acct@mydomain.com     mydomain.com/second-acct/

Again, keep adding as many mailboxes as you’d like. The big gotcha here is the trailing slash. What you’re doing here is actually specifying the path (under /var/vmail, our mail root), where the mailbox is going to live. If you leave off the trailing slash, Postfix will think you want all the mail written to a file, traditional Linux mail spool style, and this will cause hilarious problems later on when you’re trying to send and receive mail and create file locks and stuff like that. We want this to be mailbox-style, where each message gets written into a folder, and the trailing slash lets us do that. Mailboxes must be on domains that the server will actually act as a mail server for; you can’t create a mailbox for, for example, my-gmail@gmail.com.

Finally, we have virtual aliases. These basically tell Postfix to, when it receives mail addressed to one of the aliases, send it to the destination email paired with the alias instead. Open up /etc/postfix/vmail_aliases and add as many aliases as you’d like, formatted as follows:

alias@mydomain.com     me@mydomain.com
alias2@mydomain.com    my-gmail@gmail.com

You can set up aliases to either existing mailboxes on the mail server or to external addresses – both work just fine.

Once you set up all of your domains, mailboxes, and aliases, use postmap to make them into a usable format for Postfix. Run the following:

postmap /etc/postfix/vmail_domains
postmap /etc/postfix/vmail_aliases
postmap /etc/postfix/vmail_mailbox

This is literally all you have to do to get the Postfix half of our mail server ready to go. Don’t start it yet, though – we haven’t set up Dovecot, so auth will be broken.

Dovecot Setup

Dovecot will be our client for POP3 and IMAP, as well as our auth server. The config is, as above, pretty simple.


/etc/dovecot/dovecot.conf is the heart of Dovecot – what makes it all tick. In the default install, there’s a bunch of fiddly little files in the conf.d directory which handle a lot of things modularly, but our Dovecot config fits on a single page, so we’re going to ignore all of them. Back up the default configuration file, open it up in your favorite editor, rip out all of the text, and replace it with the following code (which has been commented for clarity):

listen = *
ssl = required # Do not allow unencrypted auth as this could expose login details in transit.
ssl_cert = </etc/letsencrypt/live/mail.mydomain.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.mydomain.com/privkey.pem
protocols = imap pop3
disable_plaintext_auth = no #We're encrypted, so if a client wants to use AUTH PLAIN it's not going to end our world.
auth_mechanisms = plain login
mail_access_groups = vmail
default_login_user = vmail #Again, this is our vmail user created way back in step 1.
first_valid_uid = 5000
first_valid_gid = 5000
mail_location = maildir:/var/vmail/%d/%n #This tells Dovecot to expect a directory for mail, not a file. It creates a directory if one can't be found.
passdb { #We handle our users and passwords through a passwd file. I'll cover this below.
 driver = passwd-file
 args = scheme=SHA512-CRYPT /etc/dovecot/passwd #I used SHA512 here because I didn't want to assume everyone has access to bcrypt, but you can pick your favorite scheme here: http://wiki.dovecot.org/Authentication/PasswordSchemes
userdb {
 driver = static
 args = uid=5000 gid=5000 home=/var/vmail/%d/%n allow_all_users=yes
service auth {
 unix_listener auth-client {
   group = postfix
   mode = 0666
   user = postfix
 user = root
service imap-login {
 inet_listener imaps {
   port = 993
 process_min_avail = 1
 user = vmail

service pop3-login {
 inet_listener pop3s {
 port = 995
 user = vmail

Creating users

Dovecot’s not just our POP3/IMAP server – it’s also our authentication source for the entire mail server. We’ll need to set up a passwd file – touch /etc/dovecot/passwd && chmod 600 /etc/dovecot/passwd – and then add some users.

To add users, you’ll need to generate a password; the easiest way to do this is to use the builtin doveadm. A handy shortcut to just get the hashed password is doveadm pw -s SHA512-CRYPT | cut -d '}' -f2; remember to replace SHA512-CRYPT with your preferred password scheme if you changed it in dovecot.conf above. Once you’ve got your password, you can add it to authentication using echo 'me@mydomain.com:PASSWORD' >> /etc/dovecot/passwd.

You don’t need to have a user set up in Postfix unless they need a mailbox; if you’re creating send-only addresses like noreply@mydomain.com, this is the only step you need to take. Otherwise, make sure the users here are set up in /etc/postfix/vmail_mailbox.

Testing Your Server

Now that you’ve got Dovecot configured to authenticate, we’re ready to fire up the entire mail server:

systemctl enable postfix
systemctl enable dovecot
systemctl start dovecot
systemctl start postfix

If you get errors on startup, check in /var/log/maillog and systemctl status <failing service> to see if you can pinpoint the exact cause of the error. Adding mail_debug = yesauth_debug_passwords=yes, and auth_debug = yes to your Dovecot config and checking maillog can provide a lot of insight – just make sure to remove them afterwards, especially auth_debug_passwords, and clear your log files after troubleshooting.

To test, fire up your favorite telnet client, and telnet into your server on port 25. You should be met with 250 mail.mydomain.com ESMTP Postfix. Type EHLO mail.mydomain.com, and you should get a whole bunch of 250 messages. Type AUTH LOGIN; the expected response is 334 VXNlcm5hbWU6. You’ll then need to input your base64 encoded username – remember, the username should be in the format “me@mydomain.com”. The response to this should be 334 UGFzc3dvcmQ6. Input your base64 encoded password, and you should be met with 235 2.7.0 Authentication successful. If you’ve gotten this far, your mail server works – you successfully negotiated a login with Postfix and it authenticated against Dovecot.

Adding OpenDKIM and DNS Security

I know the title of the section implies we’ll be working with DNS, but there’s one more step we need to take on the server before moving forward: setting up OpenDKIM. This is a piece of software that we’re going to integrate with Postfix in order to help validate to others that mail coming from your server is actually being sent by you.

Open up /etc/opendkim.conf, and replace the contents of the file as follows:

## Set up file locations
KeyTable           /etc/opendkim/KeyTable
SigningTable       /etc/opendkim/SigningTable
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts      /etc/opendkim/TrustedHosts

## Verification settings
Mode                    sv
SendReports             yes
ReportAddress           postmaster@mydomain.com
SoftwareHeader          yes

## Set up Unix socket
Umask                   002
UserID                  opendkim:opendkim

## Set up filtering options
PidFile                 /var/run/opendkim/opendkim.pid
SignatureAlgorithm      rsa-sha256
Canonicalization        relaxed/relaxed # This allows a little more flexibility in the filter.
OversignHeaders         From

## Automatically restart OpenDKIM if the service fails
AutoRestart             Yes
AutoRestartRate         10/1h

## Log important messages - and reasoning for them - to syslog
Syslog                  yes
LogWhy                  yes

## Open a socket for Postfix to talk to OpenDKIM on
Socket                  unix:/var/run/opendkim/opendkim.sock

Add to /etc/opendkim/TrustedHosts.

Generating keys for use in OpenDKIM is a two step process. Start by moving to /etc/opendkim/keys. Create a folder called mydomain.com and move into it, then run opendkim-genkey -s mail -d mydomain.com. The idea here is that you’ll be able to create more keys for other domains later if you need to.

Running opendkim-genkey gave us two files – mail.private and mail.txt. Run chown opendkim:opendkim mail.* to fix ownership, then it’s time to create the key table. Open /etc/opendkim/KeyTable, and add a line as follows:

mail._domainkey.mydomain.com mydomain.com:mail:/etc/opendkim/keys/mydomain.com/mail.private

The key table is basically a file that tells OpenDKIM what keys it knows about and where they can be found. As you can see here, we’ve specified the domain, the name of the key, and the location on the server. Now, we need to create the signing table, located at /etc/opendkim/SigningTable. In here, add a line:

mydomain.com mail._domainkey.mydomain.com

This table literally just maps domains to the domain key ID, which is pretty straightforward.

At this point, we’ve done all the setup for OpenDKIM; now it’s time to move on to configuring Postfix. Open up /etc/postfix/main.cf and add the following:

# Milter protocol version - based on Postfix version
# If your Postfix version is lower than 2.6, use 2 instead of 6 for this line:
milter_protocol = 6
milter_default_action = accept
smtpd_milters = unix:/var/run/opendkim/opendkim.sock
non_smtpd_milters = unix:/var/run/opendkim/opendkim.sock

At this point, we’ve configured everything we need to actually send and receive mail…except for DNS. This is a pretty big deal, because without DNS records, nobody’s going to be able to tell where they’re supposed to send email to your domain or whether mail coming from your domain is genuine. Before we get going, go ahead and put the contents of /etc/opendkim/keys/mydomain.com/mail.txt somewhere easily accessible, as you’re going to need it.

Fire up your DNS control panel and hop in. There are five records that we’re going to need to set to make sure that everything’s working as intended – an A record, an SPF record, a DKIM record, a MX record, and a DMARC record.

The obvious and easy one is setting up a “mail” A record to point at the IP of your mail server. The MX record is similarly straightforward – make sure you remove any other records, then add a record with a priority of 0 (0 is the highest possible priority, counterintuitively) and a value of “mail.mydomain.com”.

The SPF record is a little more complex. You’ll need to create a new TXT record, and set the value to v=spf1 a mx -all. What this is basically saying is “allow mail to be sent from any host that’s listed in our A or MX records, and disallow everything else”. This prevents other people from impersonating your email address. Very useful, very important to have configured correctly.

Adding a DKIM record is very similar to adding an SPF record – create a new TXT record again. This time, though, go ahead and open up the text from /etc/opendkim/keys/mydomain.com/mail.txt again. You should see something like this:

mail._domainkey    IN    TXT    ( "v=DKIM1; k=rsa; p=(a bunch of letters and numbers)" ) ; ---- DKIM key mail for mydomain.com

The first part – mail._domainkey – is what you’re going to want to put in the host field of your TXT record. The part inside the parentheses and quotes – "v=DKIM1; k=rsa; p=letters-and-numbers" – will go in the actual record.

If you’re managing multiple domains, you won’t need the A record on those domains – just set up the MX record to point to “mail.mydomain.com” and use the same SPF record as above.

At this point, you could honestly go ahead and use the mail server as is, but it’s best to implement DMARC on the domain as well. DMARC uses your SPF and DKIM policies to provide clear guidance on how other servers should handle incoming mail from your domain, which allows you to effectively nullify phishing attacks attempting to use your domain name. For more information, DMARC has a good FAQ published that’s worth your time to read.

Without going too in-depth, here’s the basic record that you’ll probably want to put in place as a TXT record with a host field of _dmarc:

v=DMARC1; p=reject; rua=mailto:dmarc@mydomain.com; adkim=r; aspf=r; sp=reject

What this does is tells the server receiving email from your domain to reject all mail that fails either your SPF or your DKIM check, and also send a report to dmarc@mydomain.com. The policy also affects subdomains. If you’re curious, DMARC provides a handy guide as a quick reference for what options are available using a record, but this is a fine basic record to use.

One important note when implementing DMARC is that it applies across every email provider you use for a domain. That means that, for example, if you’re leveraging another service than just your mail server to send email from your domain, you’ll need to make sure you modify your SPF record to include their sending servers and get a DKIM key from them as well. Most reputable mail senders will provide these records for you.

Special note for AWS users: You should also provide reverse DNS records to AWS with the hostname of your mailserver to avoid some zealous spam filter misfiling your outbound email.

Putting It Live

Finally, we’re all done with the configuration stuff – all that’s left is to kick off the new services. Run systemctl start opendkim && systemctl reload postfix to pick up the OpenDKIM settings, and then your mail server will be operational!

Special note for AWS users: You need to apply to have your rate limit removed on your EC2 instance to use it as a mail server. Typically, AWS takes care of this within 5 minutes, so it won’t hold you up.

Once you set DNS up, your mail server will be able to send and receive mail for your domains. You can set up a webmail client on the server (Roundcube and SquirrelMail are both available through the EPEL repository, last I checked), connect through a local client like Thunderbird, or connect through your favorite managed mail provider (here are the instructions for Gmail, for example).

Please let me know in the comments below if you have any questions or are running into problems, or if you think I left something out. I tried to make this writeup as detailed as I possibly could without making it too long, and hopefully I succeeded. I’m interested to hear your thoughts and feedback.

Update 7/11/17: Added DKIM signing setup

Update 7/18/17: Added DMARC record setup

Leave a Reply