A definitive Postfix/Dovecot mail server guide

posted | about 21 minutes to read

tags: amazon web services dmarc dovecot postfix tutorial system administration

This post is part of a series:
  1. A definitive Postfix/Dovecot mail server guide
  2. Adding SpamAssassin filtering to your mail server

My physical server finally bit the dust last month, so I finally took the opportunity to move up to the Amazon Web Services cloud. In the process of building my new cloud server, I realized I needed to get a mail server working - but I hadn’t ever built out a Linux mail server before past the very basics of configuring Postfix and Dovecot for a web hosting environment. I decided this was a great opportunity to make my email situation better and get my hands dirty in the process.

While researching, I discovered there were a lot of tutorials out there - unfortunately, none of them were really great. So, after getting my mail server up and running and testing the heck out of it, I decided it was time to get a comprehensive 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), 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 Linux install all the way up to virtual domains, users, and hosts. We’ll make sure an SSL certificate 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:

Things that I _will not _cover include:

Things You Need

Getting Started

Go ahead and install your preferred Linux distribution on your server. Make sure you’ve installed a firewall if necessary, as I do cover setting firewall rules as part of the guide.

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 dovecot-lmtpd -y

If the sendmail package was installed, make sure you remove it.

If you have a Debian-based distribution, you can do similar stuff with apt-get. Keep in mind that the Certbot package is named letsencrypt on Debian and derivatives. Other than that, the rest of the packages should be the same.

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. (If you’re running a web server, it may make it easier for you to renew the certificate if you actually create a web-facing empty site for this.)

Finally, make sure your firewall’s configured properly, opening ports using firewall-cmd --add-port XXX --zone=public --permanent on Fedora-based or ufw allow XXX on Debian-based distros. 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 later in the tutorial.


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
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 = lmtp:unix:private/dovecot-lmtp # I'm glossing over this but LMTP is basically what we're using to send things over to Dovecot, this will be important because it allows Dovecot to do some processing on incoming mail later if we want it to as well as allowing other functionality not covered in this tutorial
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 = private/auth
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.

tls_medium_cipherlist = AES128+EECDH:AES128+EDH

smtp_tls_security_level = may # the preceding line and this line make sure we're sending mail encrypted if possible
smtp_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1 # disable insecure protocols
smtp_tls_mandatory_ciphers = medium
smtp_tls_key_file = /etc/letsencrypt/live/mail.mydomain.com/privkey.pem
smtp_tls_cert_file = /etc/letsencrypt/live/mail.mydomain.com/fullchain.pem

smtpd_tls_security_level = may # the preceding line and this line make sure mail coming in to us is encrypted if possible
smtpd_tls_auth_only = yes
smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1 # disable insecure protocols
smtpd_tls_mandatory_ciphers = medium
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 and to make sure TLS is turned on here as well. 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 can tell, 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 allowed 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

One last thing that needs to be done is set up global aliases. These are located in /etc/postfix/aliases, and are formatted a little differently. For now, just add the line root: admin@mydomain.com to that file, replacing admin with whatever email makes sense in your environment.

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 using SSL, 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 {
   path = /var/spool/postfix/private/auth
   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
# Set up LMTP - we use this for allowing Dovecot to get mail handed off from Postfix
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    group = postfix
    mode = 0666
    user = postfix

protocol lmtp {
  postmaster_address = admin@mydomain.com # Required

# Set up the inbox structure - add more here if you want.
namespace inbox {
  inbox = yes

  mailbox Drafts {
    special_use = \Drafts
    auto = subscribe
  mailbox Spam {
    special_use = \Junk
    auto = subscribe
    autoexpunge = 30d # This will remove all junk over 30 days old.
  mailbox Trash {
    special_use = \Trash
    auto = subscribe

  mailbox "Sent Messages" {
    special_use = \Sent


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 tells other providers how to process the incoming mail; other options run the risk of not working with some mail providers like Microsoft
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/spool/postfix/milters/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.

You’ll also need to change a program default - go ahead and open up /var/default/opendkim and change the SOCKET= line to SOCKET="local:/var/spool/postfix/milters/opendkim/opendkim.sock".

Finally, go ahead and create the /var/spool/postfix/milters/opendkim directory and give opendkim:opendkim ownership.

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:/milters/opendkim/opendkim.sock
non_smtpd_milters = unix:/milters/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. You will, however, still need a unique DKIM record for each domain.

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. If you’re uncomfortable with immediately moving to reject any failed mails, you can start with “p=none;” and “sp=none;” to start, and then move to “quarantine” and “reject” when you’re comfortable with the reports that you’ve received in the email address you specified.

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 apt-get or yum, 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

Update 11/1/18: Added LMTP config and expanded Dovecot config, added Postfix config for outgoing mail, made the article applicable for both Debian and Fedora-based distros, adjusted wording