Set up a simple Debian mail server with Postfix, Dovecot and opendkim

For my 19.99USD/yr VPS, the self-hosted email server solution provided by mailinabox is simply too bloated. This post will show you how to build a simple IMAP&SMTP-only mail server with Postfix and Dovecot with DKIM support. No DNS server, no fancy webmail, no CardDAV.

Highlights

  • Supports DKIM
  • Simple configuration
  • Low memory usage (483MB total 72MB used)
  • pam based authentication
  • Mailboxes (sdbox format) stored in user homes
  • Suitable for small amount of users

Configuration

DNS Records

@ 10800 IN MX 10 mail
@ 10800 IN TXT "v=spf1 mx -all"
_dmarc 10800 IN TXT "v=DMARC1; p=reject; sp=quarantine; pct=100; rua=mailto:postmaster+rua@example.com; ruf=mailto:postmaster+ruf@example.com; fo=1"
mail 10800 IN A 93.184.216.34
mail 10800 IN AAAA 2606:2800:220:1:248:1893:25c8:1946
dkimselector._domainkey 10800 IN TXT "v=DKIM1; h=sha256; k=rsa; "
	  "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAraaOsxClVIAnIqjEjBBWTTg7x/tM1FDILMcmHgzbLZIbH7jvyjeapEtGhhwy0pOaxvv5vzZOxvDz+o2Qjbwr74h0RS0x/mexRkNMllw9Gz/pxOYLINH5VB/cgKzz+TYHTU2br8UZDZOVd3bcZSAwQv5N5N3wzlpOujEw+8cp9D+ASy+aEtFb6Ab2OxJl7juDpJd+7EGIV21+25"
	  "s4W/0a5ug1vhdVYg+TA97fcMwDSiRFHnuP/uHtlfU+hLXuHmODuHBEAI5fT6XqD+6cBa1Nyb/LjNPB5mZgtQBs5sUorh5NyTL5lrw/pg7RKFIoXwOg2iPy18FLhm6RuD1v2IPCvwIDAQAB"
smtp 10800 IN CNAME mail
imap 10800 IN CNAME mail

VPS configuration

Fresh-Clean-Brand-new Debian Buster installation REQUIRED

  1. Set up ssh access
mkdir /root/.ssh
tee -a /root/.ssh/authorized_keys << EOF
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQClNwNdN1uV/50kGEuQ7aoSIjIZAR+9zNEf/k9/rkGWFIwDxZ6jbwCp51zfZO+RuFfS3PeeChum2ukuzGgX3I0uMItxyVEdsurD7i5TmRNuT5TkigW9+LBOvIy9Qk8ueUhbkEv1P563TQVAKSMjikFYJOx/Z7dNUDcMmKap5Jauq36bE3XK++HvsyU55uN/y6D1LE8WxmkqhnmDatvY2Au6Sc6D7TxmnBbp3SZY0z9BRJ5Zf6IknC+nhqqykhU8vAdfxhpvhlCCoOf6atM+s0TqGvnNNT0L3XsQEhKcisik3mryV06IsMhTgVoWuH/cbNFGXPrsD6hNsEUL68jldj88okQuJTFCo5MROun8iRQsFJgvcEQ5MczW5DTy7lAmAWyFmmvLTW0h7yfVhgayF2JZv7+j4EaQOKaZ0iGXY0O8R2CMbz66vC2oY42fzj49jii1ozukSzyDf1Cd2XdClCOEB/bEPSk9FibovYjhxHFP11joM2zqCk6njQrXWxhfGcU= user@user1-elite-x2
EOF

cat <<EOF | tee /etc/ssh/sshd_config
PasswordAuthentication no
Port 51413
EOF

systemctl restart sshd
  1. update system
# update system
apt update && apt install -y screen && screen apt upgrade -y
# apply updates
systemctl reboot
  1. Install packages
apt install -y --no-install-recommends certbot dovecot-imapd dovecot-lmtpd postfix opendkim curl bsd-mailx opendkim-tools postfix-pcre rsync cron
  1. add mail users
groupadd -g 5000 mailusers
useradd -m -s /bin/false -g mailusers user1
useradd -m -s /bin/false -g mailusers user2
passwd user1
passwd user2
  1. generate certs
certbot --non-interactive --agree-tos -m postmaster@example.com certonly --standalone -d mail.example.com -d imap.example.com -d smtp.example.com
  1. add cert renew cron job and deploy hook
# cert renew
cat <<EOF | tee /etc/cron.daily/cert-renew
#!/bin/bash
exec certbot renew >/dev/null 2>&1
EOF
chmod +x /etc/cron.daily/cert-renew
# cert deploy hook
mkdir -p /etc/letsencrypt/renewal-hooks/post
cat <<EOF | tee /etc/letsencrypt/renewal-hooks/post/restart-mail
#!/bin/bash
systemctl restart postfix
systemctl restart dovecot
EOF
chmod +x /etc/letsencrypt/renewal-hooks/post/restart-mail
  1. generate dkimkey
# dkim key gen
cd /etc/dkimkeys
mkdir example.com
opendkim-genkey --directory=./example.com/ --domain=example.com --selector=qi2020
  1. view dkimkey dns record and add it to the DNS zone
cat dkimselector.txt
  1. fix dkim-related permissions
# fix dkimkey permission
chown opendkim:opendkim */qi2020.private
chmod 0600 */qi2020.private
  1. opendkim service config
# opendkim service config
tee -a /etc/opendkim.conf << EOF
# Automatically re-start on failures

AutoRestart yes
# limits the restarts to 10 in one hour

AutoRestartRate 10/1h
SyslogSuccess yes
Socket                  inet:8892@localhost
KeyTable file:/etc/dkimkeys/KeyTable
SigningTable refile:/etc/dkimkeys/SigningTable
EOF
tee -a /etc/dkimkeys/KeyTable << EOF
# http://www.opendkim.org/opendkim.conf.5.html

qi2020._domainkey.example.com %:qi2020:/etc/dkimkeys/%/qi2020.private
#qi2020._domainkey.example.net %:qi2020:/etc/dkimkeys/%/qi2020.private
EOF

tee -a /etc/dkimkeys/SigningTable << EOF
*@example.com qi2020._domainkey.example.com
#*@example.net qi2020._domainkey.example.net
EOF
  1. dhparams
curl https://ssl-config.mozilla.org/ffdhe2048.txt > /usr/share/dovecot/dh.pem
  1. configure dovecot
# backup original dovecot.conf
mv /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.or

cat <<EOF | tee /etc/dovecot/dovecot.conf

# Pigeonhole version 0.5.4 ()

# OS: Linux 4.19.0-9-amd64 x86_64 Debian 10.4 

# Hostname: mail.example.com

mail_location = sdbox:~/sdbox
mail_privileged_group = mail
namespace inbox {
  inbox = yes
  location = 
  mailbox Archive {
    auto = subscribe
    special_use = \Archive
  }
  mailbox Drafts {
    auto = subscribe
    special_use = \Drafts
  }
  mailbox Junk {
    auto = subscribe
    special_use = \Junk
  }
  mailbox Sent {
    auto = subscribe
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Spam {
    special_use = \Junk
  }
  mailbox Trash {
    auto = subscribe
    special_use = \Trash
  }
  prefix = 
}
passdb {
  driver = pam
}
protocols = " imap lmtp"
service auth {
  unix_listener /var/spool/postfix/private/auth {
    group = postfix
    mode = 0600
    user = postfix
  }
}
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    group = postfix
    mode = 0600
    user = postfix
  }
}
# SSL

ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_client_ca_dir = /etc/ssl/certs
ssl_dh = </usr/share/dovecot/dh.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
ssl_min_protocol = TLSv1.2
userdb {
  driver = passwd-file
  args = username_format=%n /etc/passwd
}
protocol lmtp {
  recipient_delimiter = +.
}
  1. configure postfix
cat <<EOF | tee /etc/postfix/main.cf
# See /usr/share/postfix/main.cf.dist for a commented, more complete version


# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname

smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h

readme_directory = no

# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on
# fresh installs.
compatibility_level = 2



# TLS parameters
#smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
#smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = mail.example.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = mail.example.com, example.com, mail.example.com, localhost.example.com, 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
mailbox_transport = lmtp:unix:private/dovecot-lmtp
recipient_delimiter = .+

# generated 2020-07-14, Mozilla Guideline v5.4, Postfix 3.4.8, OpenSSL 1.1.1d, intermediate configuration

# https://ssl-config.mozilla.org/#server=postfix&version=3.4.8&config=intermediate&openssl=1.1.1d&guideline=5.4

smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_mandatory_ciphers = medium

# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam

# not actually 1024 bits, this applies to all DHE >= 1024 bits

smtpd_tls_dh1024_param_file = /usr/share/dovecot/dh.pem

tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
tls_preempt_cipherlist = no

virtual_alias_maps = hash:/etc/postfix/virtual

# DKIM

smtpd_milters = inet:localhost:8892
non_smtpd_milters = inet:localhost:8892

## handle other domains
# virtual_alias_domains = example.net, example.net

# block spams 
strict_rfc821_envelopes = yes
disable_vrfy_command = yes

smtpd_helo_required = yes
smtpd_recipient_restrictions =
 reject_unknown_recipient_domain,
 reject_non_fqdn_recipient,
 permit_mynetworks,
 reject_unauth_destination
 permit
EOF

tee -a /etc/postfix/master.cf << EOF
# https://github.com/mail-in-a-box/mailinabox/blob/master/setup/mail-postfix.sh
authclean unix  n       -       y       -       0       cleanup
# https://github.com/mail-in-a-box/mailinabox/blob/master/conf/postfix_outgoing_mail_header_filters
 -o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters
 -o nested_header_checks=

submission inet n       -       -       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=reject_unknown_recipient_domain,reject_non_fqdn_recipient,permit_sasl_authenticated,reject
  -o cleanup_service_name=authclean
# for enabling 465 port
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=reject_unknown_recipient_domain,reject_non_fqdn_recipient,permit_sasl_authenticated,reject
  -o cleanup_service_name=authclean

EOF

tee -a /etc/postfix/outgoing_mail_header_filters << EOF
# https://github.com/mail-in-a-box/mailinabox/blob/master/conf/postfix_ou$
# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header
# /^Received: .*/ IGNORE
# because OpenDKIM requires that a header be present when signing outbound mail. The first line is
# where the user's home IP address would be.
/^\s*Received:[^\n]*(.*)/         REPLACE Received: from authenticated-user $1

# Remove other typically private information.
/^\s*User-Agent:/        IGNORE
/^\s*X-Enigmail:/        IGNORE
/^\s*X-Mailer:/          IGNORE
/^\s*X-Originating-IP:/  IGNORE
/^\s*X-Pgp-Agent:/       IGNORE

# The Mime-Version header can leak the user agent too, e.g. in Mime-Version: 1.0 (Mac OS X Mail 8.1 \(2010.6\)).
/^\s*(Mime-Version:\s*[0-9\.]+)\s.+/  REPLACE $1
EOF
  1. add postfix aliases
# virtual aliases
tee -a /etc/postfix/virtual << EOF
alias1@example.com   user1
alias2@example.com   user1
webmaster@example.com   user1
admin@example.com   user1
EOF
# user aliases
tee -a /etc/aliases << EOF
nobody: root
root: user1
EOF
# generate postmap db
newaliases
postmap /etc/postfix/virtual
  1. configure unattended-upgrades
apt install -y unattended-upgrades
dpkg-reconfigure unattended-upgrades
tee -a /etc/apt/apt.conf.d/50unattended-upgrades << EOF
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::Automatic-Reboot "true";
EOF
  1. apply everything
systemctl restart opendkim
systemctl restart dovecot
systemctl restart postfix
  1. Backup
# backup
# also install rsync on the target
apt install -y rsync
ssh-keygen -t ed25519
cat .ssh/id_ed25519.pub
echo 'Host mbox.1103.example.com
     Port 54321
     User mailbackup' >> ~/.ssh/config
echo '#!/bin/bash
rsync -a --delete --quiet -e ssh /home mbox.1103.example.com:~' >> /etc/cron.hourly/mailbackup
chmod +x /etc/cron.hourly/mailbackup 
/etc/cron.hourly/mailbackup

Email client configuration

IMAP

  • Server: imap.example.com or mail.example.com or smtp.example.com
  • Security: SSL/TLS
  • Port: 993
  • Username: “user1”
  • Password: passw0rd
  • authentication method: normal password (plaintext)

SMTP

  • Server: imap.example.com or mail.example.com or smtp.example.com
  • Security: STARTTLS or SSL/TLS
  • Port: 587 or 465
  • Username: “user1”
  • Password: passw0rd
  • authentication method: normal password (plaintext)