Situatie
These are some notes on setting up a small mail server suitable for a single user or a few users.
This setup uses the following projects to enable sending and receiving mail using SPF, DKIM, and DMARC for email authentication:
- Postfix for mail transfer and delivery to Dovecot via LMTP.
- Dovecot for IMAP access and SASL for Postfix with Pigeonhole for Sieve message filtering support
- Rspamd for spam filtering, email authentication validation, and DKIM signing (with Redis for caching)
- Let’s Encrypt and certbot to provide SSL certs
Moreover this configuration enables support for sending and receiving mail on two domains whereby the two domains mirror each other.
CERTIFICATES AND KEYS
SSL/TLS CERTS
Obtain an SSL certificate from Let’s Encrypt. This particular server did not have an existing HTTP server, so I used certbot in standalone mode:
# certbot certonly --standalone -d domain1.tld,domain2.tld
This produces a single certificate for both domains. Alternatively, one could invoke certbot for each domain to produce separate certificates.
DKIM KEYS
Rspamd includes a utility to generate DKIM keys. I created a directory within /etc/rspamd
, protected it, and generated keys according to Rspamd’s dkim_signing
guide.
# mkdir -p /etc/rspamd/dkim/keys
# chown -R rspamd:rspamd /etc/rspamd/dkim
# chmod -R 700 /etc/rspamd/dkim
# rspamadm dkim_keygen -s 'mail' -b 2048 -d domain1.tld -k /etc/rspamd/dkim/keys/domain1.tld.mail.key > /etc/rspamd/dkim/keys/domain1.tld.mail.txt
# rspamadm dkim_keygen -s 'mail' -b 2048 -d domain1.tld -k /etc/rspamd/dkim/keys/domain2.tld.mail.key > /etc/rspamd/dkim/keys/domain2.tld.mail.txt
# chown rspamd:rspamd /etc/rspamd/dkim/keys/*
# chmod 600 /etc/rspamd/dkim/keys/*
If you are having trouble splitting up the public key into multiple chunks within the DNS TXT record, you may need to use -b 1024
and live with the weakened security.
DIFFIE-HELLMAN PARAMETERS FOR DOVECOT
This is actually optional because I disabled non-ECC Diffie-Hellman ciphers in the Dovecot configuration.
# touch /etc/dovecot/dh.pem
# chmod 600 /etc/dovecot/dh.pem
# chown root:root /etc/dovecot/dh.pem
# openssl dhparam -out /etc/dovecot/dh.pem 4096
POSTFIX CONFIGURATION
The following Postfix configuration enables SASL through Dovecot, delivers mail to Dovecot’s LMTP service, enables TLS, and enables Rspamd as a milter.
I’ve only listed variables that differ from the defaults as of Postfix 3.7.
# based on Postfix 3.7.0
compatibility_level = 3.7
# Restrictions
# smtpd_recipient_restrictions includes the following features:
# 1. prohibit specific senders via check_sender_access
# create a list of prohibited domains in the following format
# baddomain.tld REJECT
# save to /etc/postfix/sender_access and run `postmap /etc/postfix/sender_access`
# 2. prepend X-Original-To for LMTP via check_recipient_access and set
# lmtp_destination_recipient_limit
# see https://dovecot.dovecot.narkive.com/jYiqyZYr/differences-in-delivered-to-header-between-deliver-and-lmtp#post7
smtpd_recipient_restrictions = check_sender_access hash:/etc/postfix/sender_access,
check_recipient_access pcre:{{/(.+)/ prepend X-Original-To: $$1}}
lmtp_destination_recipient_limit = 1
# Aliases
alias_maps = hash:/etc/postfix/aliases
alias_database = $alias_maps
# Network
# set because $myhostname is domain.tld (not server.domain.tld)
mydomain = $myhostname
# set in order to add secondary domain
# alternative is to use virtual domains: http://www.postfix.org/VIRTUAL_README.html
mydestination = $myhostname, localhost.$mydomain, localhost, localhost.localdomain, domain2.tld
# TLS support
smtpd_use_tls = yes
smtpd_tls_loglevel = 1
smtpd_tls_cert_file = /etc/letsencrypt/live/domain1.tld/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/domain1.tld/privkey.pem
smtpd_tls_session_cache_database = btree:/var/lib/postfix/smtpd_scache
smtp_tls_session_cache_database = btree:/var/lib/postfix/smtp_scache
# SASL support
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
# Milters
#smtpd_milters = unix:/var/lib/rspamd/milter.sock
# or for TCP socket
smtpd_milters = inet:localhost:11332
non_smtpd_milters = $smtpd_milters
#milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
# skip mail without checks if something goes wrong
milter_default_action = accept
# Delivery
mailbox_transport = lmtp:unix:private/dovecot-lmtp
# Others
recipient_delimiter = +
biff = no
To enable “Submission” (port 587) for client usage, the following can be added to /etc/postfix/master.cf
:
submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_auth_only=yes
-o smtpd_reject_unlisted_recipient=no
-o smtpd_recipient_restrictions=
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
Edit 2021-09-29: smtpd_tls_cert_file
now points at the full chain instead of the individual cert.
Edit 2022-02-13: smtpd_recipient_restrictions
uses Postfix 3.7’s inline pcre:{{}}
syntax rather than requiring a separate file.
SUPPORTING MULTIPLE DOMAINS
In this configuration, I opted to serve multiple domains in the simplest way, by adding the second domain to mydestination
. Postfix’s guide on this subject describes this way as being useful for the situation where each user receives mail in each domain, which was the case that applied to me. For more complex usages, one could use virtual alias domains, also described in that guide.
RELAY VS. RECIPIENT RESTRICTIONS
Postfix has a document that describes access restrictions. It notes that as of Postfix 2.10, the smtpd_relay_restrictions
takes care of preventing Postfix from acting as an open relay. As such, many Postfix configuration tutorials do not have up-to-date guidance; instead, they opt to carefully configure smtpd_recipient_restrictions
. As Postfix’s document illustrates, either way will work, so long as one of them prevents Postfix from acting as an open relay. In this configuration, I am relying on the sane default configuration of smtpd_relay_restrictions
, which postconf -d smtpd_relay_restrictions
reports to be:
smtpd_relay_restrictions = ${{$compatibility_level} < {1} ? {} : {permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination}}
Optionally, you can use smtpd_recipient_restrictions
to prohibit certain domains. In my case, an attacker has been trying to sign up for an unprotected service by using fake emails from my domain name. I reject all emails from that service.
Edit 2020-07-06: Per this discussion, I have added a check_recipient_access
entry that adds an X-Original-To
header containing the original address that the email was intended for. Paired with the adjustment of the Dovecot configuration variable lda_original_recipient_header
, this makes Sieve filtering based on address and “detail” (label after the +
in an address) fairly simple.
I structured /etc/dovecot
to follow the example template provided with Dovecot.
# mkdir -p /etc/dovecot
# cp -R /usr/share/doc/dovecot/example-config/conf.d /etc/dovecot
# cp -R /usr/share/doc/dovecot/example-config/dovecot.conf /etc/dovecot
Importantly, I had to make a few changes to the files in conf.d/
:
- Because I am using passwd-file authentication, I commented out the inclusion of
auth-system.conf.ext
inconf.d/10-auth.conf
:#!include auth-system.conf.ext
- Because SSL certs are located in
/etc/letsencrypt
, I commented out the attempts to read in those certs inconf.d/10-ssl.conf
:#ssl_cert = </etc/ssl/certs/dovecot.pem #ssl_key = </etc/ssl/private/dovecot.pem
I suppose one could symlink the certs to these locations instead.
I then made changes from the default configuration (as of Dovecot 2.3.10.1) by creating the following /etc/dovecot/local.conf
. Dovecot will merge this configuration with the existing defaults.
protocols = imap lmtp
# conf.d/10-auth.conf
auth_mechanisms = plain login
!include conf.d/auth-passwdfile.conf.ext
# conf.d/10-mail.conf
mail_location = maildir:~/.mail
# conf.d/10-master.conf
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
# conf.d/10-ssl.conf
ssl_cert = </etc/letsencrypt/live/domain1.tld/fullchain.pem
ssl_key = </etc/letsencrypt/live/domain1.tld/privkey.pem
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ALL:!DH:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH
ssl_prefer_server_ciphers = yes
# conf.d/15-lda.conf
lda_original_recipient_header = X-Original-To
# conf.d/20-imap.conf
protocol imap {
mail_max_userip_connections = 20
}
# conf.d/20-lmtp.conf
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
As a consequence, Dovecot provides a SASL service to Postfix for authentication and receives mail from Postfix via LMTP. It enables an IMAP service for mail user agent connection.
Edit 2020-07-06: The adjustment to lda_original_recipient_header
(which also applies to LMTP) tells Dovecot to use the X-Original-To
header to specify the original recipient.
Following Mozilla’s TLS guide, the minimum protocol is set to TLSv1.2
. A more restrictive set of ciphers are allowed as well (in particular, no non-ECC Diffie-Hellman support).
Edit 2021-09-29: ssl_cert
now points at the full chain instead of the individual cert.
USER AUTHENTICATION
This configuration uses a simple password file for authentication.
touch /etc/dovecot/users
chown dovecot:dovecot /etc/dovecot/users
chmod 600 /etc/dovecot/users
Passwords can be generated with dovecot pw -s SHA512-CRYPT
as per Dovecot’s password scheme guide.
Then, following Dovecot’s Passwd-file guide, one can create a mostly-empty row:
in /etc/dovecot/users
:
user:{SHA512-CRYPT}<hash>:sys_user:sys_user_group::/home/sys_user::
This configuration allows for a virtual mapping from user
to the system account sys_user
with its corresponding group sys_user_group
and home directory /home/sys_user
. user
should be the account that you map all of your aliases in /etc/postfix/aliases
to.
Edit 2022-01-29: The Rspamd-Redis connection now uses UNIX sockets.
Per Rspamd’s Quick Start guide, I adjusted a few Redis configuration settings slightly, namely setting a memory limit and policy and enabling access via UNIX socket. To do that, I added include /etc/redis.d/local.conf
to the end of /etc/redis.conf
and created the following /etc/redis.d/local.conf
:
# settings recommended by Rspamd
# https://rspamd.com/doc/quickstart.html
maxmemory 500mb
maxmemory-policy volatile-ttl
unixsocket /var/run/redis/redis.sock
unixsocketperm 770
I also followed the suggestion of setting vm.overcommit_memory = 1
with sysctl and in /etc/sysctl.d/
.
RSPAMD
Rspamd is a powerful spam filtering tool that can also be used to add DKIM signatures to outgoing messages. I have used it since ~v1.2 (mid-2016); it has grown significantly since then, and there have been a few configuration-breaking changes in that time. However, I have not encountered such issues recently.
I made changes exclusively within /etc/rspamd/local.d
and only included settings that changed the defaults as of Rspamd 2.5.
GLOBAL CONFIGURATION SETTINGS
I have Unbound configured as the local nameserver, so Rspamd will automatically use it (/etc/resolv.conf
points to 127.0.0.1
). Rspamd can generate a lot of DNS requests, so I have found this to be a valuable solution. Using a public nameserver will likely result in rejections over time.
Therefore, local.d/options.inc
looks like:
# same as postfix $mynetworks minus loopback addresses (`postconf mynetworks`)
local_addrs = [
<self IP addresses>
];
# DNS tuning for local DNS server
# probably not necessary
dns {
timeout = 10s;
retransmits = 50;
}
# server does not support SSE3
disable_hyperscan = true;
PROXY WORKER AS MILTER
The Rspamd proxy worker acts as a milter by default, but should be configured to scan outbound mail to DKIM sign messages. That can be accomplished by setting the following in local.d/worker-proxy.inc
:
upstream "local" {
self_scan = yes;
}
# spawn more processes in self-scan mode
count = 4;
DKIM SIGNING
Because each DKIM key follows a structured file naming format, local.d/dkim_signing.conf
is relatively simple:
# the same settings apply for all domains
selector = "mail";
path = "/etc/rspamd/dkim/keys/$domain.$selector.key";
allow_username_mismatch = true;
SPAM BLOCKING CONFIGURATION
For testing, it’s best to avoid outright rejecting emails that have a high spam score. The following adjustment in local.d/actions.conf
adds spam headers to pretty much all messages that exceed the (default) add_header
threshold.
# always add headers instead of rejecting
reject = 500;
The following files enable and configure their respective modules to use Redis as a backend.
Edit 2022-01-29: The Rspamd-Redis connection now uses UNIX sockets.
Rspamd connects to Redis via a UNIX socket (per these instructions). To provide Rspamd access, its user must be added to the redis
group via usermod -a -G redis rspamd
(note that some installations may have different usernames for Rspamd).
local.d/classifier-bayes.conf
:
backend = "redis";
local.d/mx_check.conf
:
enabled = true;
local.d/redis.conf
:
servers = "/var/run/redis/redis.sock";
Adding the following to ~/.dovecot.sieve
will send mail marked as spam to the Junk folder:
require ["fileinto"];
if header :is "X-Spam" "Yes" {
fileinto "Junk";
stop;
}
The firewall should open SMTP ports 25 and 587 and IMAP ports 143 and 993 to appropriate network traffic. For nftables, the following configuration can be added to an input inet filter chain:
tcp dport { 25, 587 } accept comment "Allow SMTP/Submission"
tcp dport { 143, 993 } accept comment "Allow IMAP/IMAPS"
DNS entries need to be added for reverse DNS, SPF, DKIM, and DMARC. These tools all help other mail servers realize that your mail is not spam. Of course, an MX record is required as well.
For the domain name that matches $myhostname
in Postfix (not any other domains), a PTR record is necessary. For example, for an IP address of 1.2.3.4:
4.3.2.1.in-addr.arpa. 3599 IN PTR domain1.tld.
An equivalent IPv6 PTR record is necessary, too. This tool is helpful in generating the record.
For all domains, an SPF record is required:
domain1.tld. 3599 IN TXT "v=spf1 ip4:<IPv4 Address> ip6:<IPv6 Address> -all"
Each of the DKIM keys generated above also generated a DNS record in /etc/rspamd/dkim/keys/$domain.$selector.txt
. Each domain’s record must be added.
Adding a DMARC record to each domain helps other mail servers understand what to do with mail that failed SPF or DKIM checks. For testing, it makes sense to ask the mail server to keep the mail but send reports back to you.
_dmarc.domain1.tld. 3599 IN TXT "v=DMARC1; p=none; pct=100; rua=mailto:dmarc-reports@domain1.tld"
By changing to p=reject
, the other server will reject mail that failed these checks.
Leave A Comment?