Let's face it, everyone knows email is an absolute pain to set up on linux-based servers. Hopefully this guide can help remedy that.
If you don't already have any SSL/TLS certificates, you can follow my Let's Encrypt Guide to get free and trusted certificates for both mail and web.
This guide will show you step by step how to set up a secure email server that supports virtual mailboxes, SPF, DKIM, DMARC and SSL/TLS.
For your emails to be transferred encrypted over a network, you need SSL/TLS. For that you need a certificate and private key pair. This can be obtained several ways, and i go over one of many ways in another guide.
The most important thing is that you store this pair along with the certificate authority's certificate in a place readable by postfix and dovecot. You will need the path to your certificate, your private key and the CA's certificate (chain).
If you already have a MySQL server set up, you may continue reading. Otherwise please follow my guide on installing MySQL 5.7.
First of all we need to create a database to hold our tables and data. For security we'll create a specific user only used to read the database by the email servers. We also need to USE the database, so our tables will be created in the correct database.
CREATE DATABASE mail;
GRANT SELECT ON mail.* TO 'mail'@'localhost' IDENTIFIED BY 'mailpassword';
FLUSH PRIVILEGES;
USE mail;
We'll be creating three tables for our virtual mailbox configuration. One for aliases, one for domains and one for users.
CREATE TABLE IF NOT EXISTS `virtual_domains` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `virtual_aliases` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`source` varchar(254) NOT NULL,
`destination` varchar(254) NOT NULL,
PRIMARY KEY (`id`),
KEY `domain_id` (`domain_id`),
CONSTRAINT `virtual_aliases_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `virtual_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain_id` int(11) NOT NULL,
`password` varchar(106) NOT NULL,
`email` varchar(254) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
KEY `domain_id` (`domain_id`),
CONSTRAINT `virtual_users_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Now we'll begin inserting data into the tables, starting with virtual domains. You should obviously change mydomain.com and remove or change my2nddomain.com as neccesary. You may also add new values, depending on what domains you wish to receive emails for.
INSERT INTO `virtual_domains`
(`id`, `name`)
VALUES
('1', 'mydomain.com'),
('2', 'my2nddomain.com');
Next we'll create email accounts or mailboxes. Again, modify the query to your preferences.
INSERT INTO `virtual_users`
(`id`, `domain_id`, `password` , `email`)
VALUES
('1', '1', ENCRYPT('firstpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]'),
('2', '2', ENCRYPT('secondpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '[email protected]');
We can also create aliases for the mailboxes. This basically routes mails from the source address to the destination address, no matter if there's already an account on that source address. However, the destination address must be an existing mailbox.
INSERT INTO `virtual_aliases`
(`id`, `domain_id`, `source`, `destination`)
VALUES
('1', '1', '[email protected]', '[email protected]');
apt-get install -y dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql
First we'll make sure dovecot is set to include extra config files and enable some protocols.
nano /etc/dovecot/dovecot.conf
Now we'll enable the protocols we need. These protocols won't be enabled if they're not defined first though, that's why we have to enable them after the !include_try /usr/share/dovecot/protocols.d/*.protocol
line. We want to enable imap and lmtp, so we are going to write it like this:
!include_try /usr/share/dovecot/protocols.d/*.protocol
protocols = imap lmtp
Finally, to make sure dovecot includes all config files in /etc/dovecot/conf.d/
we have to make sure the following line is uncommented:
!include conf.d/*.conf
Next up we're going to create a separate folder for every virtual domain and set the correct permissions, then tell that to dovecot.
groupadd -g 5000 mail
useradd -g mail -u 5000 mail -d /var/mail
mkdir -p /var/mail/vhosts/{mydomain.com,my2nddomain.com}
chown -R mail:mail /var/mail
chown -R dovecot:mail /etc/dovecot
chmod -R o-rwx /etc/dovecot
nano /etc/dovecot/conf.d/10-mail.conf
Now we need to edit mail_location
and mail_privileged_group
in order for dovecot to understand where mail is located, and what user has access to the mail.
Find the mail_location
line, uncomment it and change it to the following:
mail_location = maildir:/var/mail/vhosts/%d/%n
Find the mail_privileged_group
line, uncomment it and change it to the following:
mail_privileged_group = mail
Finally, find the first_valid_uid
line, uncomment it and change it to the following:
first_valid_uid = 1
Now we need to tell dovecot that we're using MySQL to authenticate our users.
nano /etc/dovecot/conf.d/10-auth.conf
Uncomment the following line:
disable_plaintext_auth = yes
Find the line containing auth_mechanisms = plain
and change it to the following:
auth_mechanisms = plain login
Comment out this line:
#!include auth-system.conf.ext
Uncomment this line in order to enable MySQL authentication:
!include auth-sql.conf.ext
To allow dovecot to connect to our MySQL database, we need to give it our MySQL credentials using a driver.
nano /etc/dovecot/conf.d/auth-sql.conf.ext
Enter the following in the file before saving it:
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=mail gid=mail home=/var/mail/vhosts/%d/%n
}
Now to actually provide dovecot our credentials, we have to modify another file.
nano /etc/dovecot/dovecot-sql.conf.ext
Find the #driver =
line, uncomment it and change it to the following:
driver = mysql
Find the #connect =
line, uncomment it and change it to the following, replacing the highlighted
parts with your own MySQL credentials we created here.
connect = host=127.0.0.1 dbname=mail user=mail password=mailpassword
Find the #default_pass_scheme =
line, uncomment it and change it to the following:
default_pass_scheme = SHA512-CRYPT
Finally, find the #password_query = \
line, uncomment it and change it to the following:
password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';
Now we're going to define the services that dovecot will provide.
nano /etc/dovecot/conf.d/10-master.conf
Find service imap-login
and change it to the following:
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
Find service lmtp
and change it to the following:
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
Find service auth
and change it to the following:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0600
user = mail
}
user = dovecot
}
Finally, find service auth-worker
and change it to the following:
service auth-worker {
user = mail
}
This will require dovecot to encrypt all network traffic with the ssl certificate we obtained here.
nano /etc/dovecot/conf.d/10-ssl.conf
Change the ssl
parameter to required:
ssl = required
Modify the path for ssl_cert
to your full certificate chain minus your CA's certificate and ssl_key
to the path to your certificate's private key.
ssl_cert = </etc/letsencrypt/live/mail.mydomain.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.mydomain.com/privkey
Use stronger ssl_dh_parameters_length
ssl_dh_parameters_length = 4096
Disable insecure ssl_protocols
ssl_protocols = !SSLv2 !SSLv3 !TLSv1
Use a stronger ssl_cipher_list
ssl_cipher_list = ALL:HIGH:!SSLv2:!MEDIUM:!LOW:!EXP:!RC4:!MD5:!aNULL:@STRENGTH
Prefer server ciphers
ssl_prefer_server_ciphers = yes
apt-get install -y postfix postfix-mysql
Shortly after entering this command, you'll be prompted with the following. In this guide i will be showing how to set up a normal "Internet Site", so choose that option.
After selecting "Internet Site" you'll be asked to enter your "System mail name". This is your FQDN (Fully Qualified Domain Name) for the email server. I usually pick "mail.mydomain.com" here.
We'll start with moving the default posfix configuration and making our own.
mv /etc/postfix/main.cf /etc/postfix/main.cf.default
nano /etc/postfix/main.cf
Add the following lines replacing mail.mydomain.com
with your FQDN and everything marked in red
with the appropriate paths from here and save the file.
# SMTPD banner
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu/Debian)
# Disable local mail notifications
biff = no
# Appending .domain is the MUA's job.
append_dot_mydomain = no
# Enable SASL authentication
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
# SMTPD SSL/TLS
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_loglevel = 1
smtpd_tls_CAfile = /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.mydomain.com/cert.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.mydomain.com/privkey.pem
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
# SMTP SSL/TLS
smtp_use_tls = yes
smtp_tls_security_level = may
smtp_tls_loglevel = 1
smtp_tls_CAfile = /etc/ssl/certs/lets-encrypt-x1-cross-signed.pem
smtp_tls_cert_file = /etc/letsencrypt/live/mail.mydomain.com/cert.pem
smtp_tls_key_file = /etc/letsencrypt/live/mail.mydomain.com/privkey.pem
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# Block clients that speak too early.
smtpd_data_restrictions = reject_unauth_pipelining
# Don't accept mail from domains that don't exist.
smtpd_sender_restrictions = reject_unknown_sender_domain
# Block senders with invalid or blacklisted hostname, unknown domain, non-fqdn domain, unauthorized destination.
# Also ensures sender SPF is valid.
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_pipelining,
reject_invalid_hostname,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unauth_destination,
reject_unknown_recipient_domain,
reject_rbl_client zen.spamhaus.org,
reject_rhsbl_reverse_client dbl.spamhaus.org,
reject_rhsbl_helo dbl.spamhaus.org,
reject_rhsbl_sender dbl.spamhaus.org,
check_policy_service unix:private/policy-spf,
reject_non_fqdn_recipient
# Network/Database config
myhostname = mail.mydomain.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
local_recipient_maps = proxy:unix:passwd.byname $alias_maps
# Virtual mailboxes and database mappings
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf
# Default milter options
milter_protocol = 2
milter_default_action = accept
# DKIM, DMARC, Spamassassin
smtpd_milters = unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock, unix:/spamass/spamass.sock
non_smtpd_milters = unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock, unix:/spamass/spamass.sock
# SPF time limit
policy-spf_time_limit = 3600s
Now we'll configure the master process of postfix, and any daemons we want to run under postfix.
We'll be moving the default configuration because i provide a full configuration file.
mv /etc/postfix/master.cf /etc/postfix/master.cf.default
nano /etc/postfix/master.cf
Insert the following lines and save the file.
Copy to clipboard#
# Postfix master process configuration file. For details on the format
# of the file, see the master(5) manual page (command: "man 5 master").
#
# Do not forget to execute "postfix reload" after editing this file.
#
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (yes) (never) (100)
# ==========================================================================
smtp inet n - - - - smtpd
submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_tls_wrappermode=no
smtps inet n - - - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_tls_wrappermode=yes
#628 inet n - - - - qmqpd
pickup unix n - - 60 1 pickup
cleanup unix n - - - 0 cleanup
qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - - 1000? 1 tlsmgr
rewrite unix - - - - - trivial-rewrite
bounce unix - - - - 0 bounce
defer unix - - - - 0 bounce
trace unix - - - - 0 bounce
verify unix - - - - 1 verify
flush unix n - - 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - - - - smtp
relay unix - - - - - smtp
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
showq unix n - - - - showq
error unix - - - - - error
retry unix - - - - - error
discard unix - - - - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - - - - lmtp
anvil unix - - - - 1 anvil
scache unix - - - - 1 scache
#
# ====================================================================
# Interfaces to non-Postfix software. Be sure to examine the manual
# pages of the non-Postfix software to find out what options it wants.
#
# Many of the following services use the Postfix pipe(8) delivery
# agent. See the pipe(8) man page for information about ${recipient}
# and other message envelope options.
# ====================================================================
#
# maildrop. See the Postfix MAILDROP_README file for details.
# Also specify in main.cf: maildrop_destination_recipient_limit=1
#
maildrop unix - n n - - pipe
flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
#
# ====================================================================
#
# Recent Cyrus versions can use the existing "lmtp" master.cf entry.
#
# Specify in cyrus.conf:
# lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
#
# Specify in main.cf one or more of the following:
# mailbox_transport = lmtp:inet:localhost
# virtual_transport = lmtp:inet:localhost
#
# ====================================================================
#
# Cyrus 2.1.5 (Amos Gouaux)
# Also specify in main.cf: cyrus_destination_recipient_limit=1
#
#cyrus unix - n n - - pipe
# user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
#
# ====================================================================
# Old example of delivery via Cyrus.
#
#old-cyrus unix - n n - - pipe
# flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
#
# ====================================================================
#
# See the Postfix UUCP_README file for configuration details.
#
uucp unix - n n - - pipe
flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
#
# Other external delivery methods.
#
ifmail unix - n n - - pipe
flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp unix - n n - - pipe
flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
scalemail-backend unix - n n - 2 pipe
flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
mailman unix - n n - - pipe
flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
${nexthop} ${user}
policy-spf unix - n n - - spawn
user=nobody argv=/usr/bin/policyd-spf
Create the following files replacing mailpassword
with the password you set up in the MySQL step.
user = mail
password = mailpassword
hosts = 127.0.0.1
dbname = mail
query = SELECT 1 FROM virtual_domains WHERE name='%s'
user = mail
password = mailpassword
hosts = 127.0.0.1
dbname = mail
query = SELECT 1 FROM virtual_users WHERE email='%s'
user = mail
password = mailpassword
hosts = 127.0.0.1
dbname = mail
query = SELECT destination FROM virtual_aliases WHERE source='%s'
apt-get install -y opendkim opendkim-tools
mkdir -p /etc/opendkim/keys
adduser postfix opendkim
nano /etc/opendkim.conf
Insert the following at the end of the file.
AutoRestart yes
AutoRestartRate 10/1h
Syslog yes
SyslogSuccess yes
LogWhy yes
Canonicalization relaxed/simple
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Mode sv
SignatureAlgorithm rsa-sha256
UserID opendkim:opendkim
If you want to learn about these options and what they do, look at the documentation.
mkdir -p /var/spool/postfix/var/run/opendkim
chown -R opendkim:opendkim /var/spool/postfix/var/run/opendkim
nano /etc/default/opendkim
Add the following line to the end of the file
SOCKET="local:/var/spool/postfix/var/run/opendkim/opendkim.sock"
Now we'll define the hosts to sign all mails for.
nano /etc/opendkim/TrustedHosts
Insert the following lines and save the file.
127.0.0.1
localhost
*.mydomain.com
We also need to create a key table that will contain each selector/domain pair and the path to their private key. The most common selector to use is "mail", so i would reccomend using that.
nano /etc/opendkim/KeyTable
Insert the following lines replacing mydomain.com with your root domain and save the file.
You should add a new line with the same structure for every new domain you wish to send mails for.
mail._domainkey.mydomain.com mydomain.com:mail:/etc/opendkim/keys/mydomain.com/mail.private
Now create a signing table that's used for declaring the domains/email addresses to sign messages for and their selectors.
nano /etc/opendkim/SigningTable
Insert the following lines replacing mydomain.com with your root domain and save the file.
You should add a new line with the same structure for every new domain you wish to send mails for.
*@mydomain.com mail._domainkey.mydomain.com
In this case, we will sign all emails for any email ending with @mydomain.com with the private key for mail._domainkey.mydomain.com.
The final step in setting up opendkim is generating the keys that will be signing the emails. You should repeat this process for every domain you've added in the key table and signing table.
cd /etc/opendkim/keys
mkdir mydomain.com
cd mydomain.com
opendkim-genkey -s mail -d mydomain.com -b 4096
chown opendkim:opendkim mail.private
Executing these commands will make a folder for your domain and generate two files inside it. The one called mail.private is your private key used for signing emails. The one called mail.txt will contain a DNS TXT record you must put on your domain.
The record put in a zone file should look somewhat like this (along with an Author Domain Signing Practices record).
mail._domainkey IN TXT "v=DKIM1; k=rsa; p=LONG/STRING/OF/CHARACTERS"
_adsp._domainkey IN TXT "dkim=all"
apt-get install -y postfix-policyd-spf-python
SPF is a method of fighting spam, ensuring that whoever sends you an email is sending it from a whitelisted email server. This works both ways as long as both parties have SPF enabled, meaning other email servers will also verify that emails you send come from your server.
@ IN TXT "v=spf1 a mx ip4:123.123.123.123 -all"
The SPF record above will only allow servers with an IP address equal to the one of your A or MX records or the IP address 123.123.123.123
to send emails from your domain.
apt-get install -y opendmarc
mkdir -p /var/spool/postfix/var/run/opendmarc
chown -R opendmarc:postfix /var/spool/postfix/var/run/opendmarc
adduser postfix opendmarc
nano /etc/default/opendmarc
Add the following line to the end of the file
SOCKET="local:/var/spool/postfix/var/run/opendmarc/opendmarc.sock"
DMARC is really just a DNS TXT record enforcing SPF and DKIM validation on emails coming from your domain.
Here's an example record in zone file format. The options in this record are explained here.
_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400"
apt-get install -y spamassassin spamass-milter
groupadd spamd
useradd -g spamd -s /bin/false -d /var/lib/spamassassin spamd
adduser spamd spamass-milter
adduser postfix spamd
mkdir /var/lib/spamassassin
chown -R spamd:spamd /var/lib/spamassassin
nano /etc/default/spamassassin
Replace everything with the following:
ENABLED=1
SAHOME="/var/lib/spamassassin/"
OPTIONS="--username spamd --nouser-config --max-children 2 --helper-home-dir ${SAHOME} --socketpath=/var/run/spamassassin.sock --socketowner=spamd --socketgroup=spamd --socketmode=0660"
CRON=1
Edit the spamass-milter config
nano /etc/default/spamass-milter
Replace everything with the following:
OPTIONS="-u spamass-milter -m -I -i 127.0.0.1 -- --socket=/var/run/spamassassin.sock"
To get the most out of spamassassin you have to create rules.
nano /etc/spamassassin/local.cf
Uncomment the following lines:
# rewrite_header Subject *****SPAM*****
# required_score 5.0
# use_bayes 1
# bayes_auto_learn 1
Then change this:
rewrite_header Subject *****SPAM*****
required_score 5.0
To this:
rewrite_header Subject [***** SPAM _SCORE_ *****]
required_score 3.0
Now that everything is configured correctly, we need to restart everything to apply our changes.
service spamassassin restart
service opendkim restart
service opendmarc restart
service dovecot restart
service postfix restart
Now you should be ready to test!
To ensure everything is working correctly, open up your email client and send an email to [email protected]. If everything is working as it should be, you should receive a response like this:
==========================================================
Summary of Results
==========================================================
SPF check: pass
DomainKeys check: neutral
DKIM check: pass
Sender-ID check: pass
SpamAssassin check: ham
If your results don't match the above, you should re-read the steps of the guide where it failed. If that doesn't give you any answers, remember Google is your friend.
I hope you were able to follow the guide without any problems, if you have any questions please feel free to contact me!