Host your own mailserver with Dovecot, Postfix, Rspamd and PostgreSQL
Since around 2010 I have been running mail servers for myself and other organisations. In this post my notes on how I generally approach this are described. It shows how to set up a modern mail server capable of handling e-mail for multiple domains on Debian 12 “Bookworm”. It is also fully compatible with Debian 11 “Bullseye” using the following software packages:
- Postfix SMTP server
- Used to send and receive e-mail.
- Dovecot IMAP server
- Enables us to store and sync e-mail across devices.
- Provides ways to filter e-mails and perform automated actions.
- Rspamd spam filtering system
- Signs outgoing e-mail using DKIM.
- Validates incoming e-mails.
- Scores e-mails in order to decide whether e-mails are “ham” or “spam”.
- Redis in-memory key-value database
- Stores Rspamd training data.
- PostgreSQL
- Functions as our database for domain names and users for which we accept e-mail.
- Stores TLS policies.
- Lego
- Provides us with SSL certificates from Letsencrypt
- Nginx
- Serves as a HTTP server for autoconfiguration files.
- Proxies the Rspamd control panel.
- Unbound validating, recursive and caching DNS resolver
- Required because Rspamd makes many DNS lookup requests when filtering e-mail.
- Postfix needs to be able to do DNS lookups through a DNS recursor which supports DNSSEC.
It supports many modern e-mail standards such as:
- SPF, DKIM and DMARC
- DNSSEC
- DANE SMTP server validation, both for receiving as well as sending e-mail.
- MTA-STS, both for receiving as well as sending e-mail.
- To check for MTA-STS policies, we make use of the Postfix MTA-STS Resolver.
- Secure and modern TLS ciphers. All user connections will use STARTTLS. TLS will be promoted in communications between other servers.
- It sets up a global spam filter, which uses self-learning from e-mails in all mailboxes.
- “Address tagging”, adding tags to existing e-mail addresses using the
+
sign. E.g.:youremailaddress+somewebsite@example.org
oryouremailaddress+newsletters@example.org
. This allows you to easily define mailbox filters for different kinds of e-mail, for example, to automatically sort incoming newsletters into a newsletter folder instead of your inbox.
The setup also offers some convenient additional functionality such as:
- Aliases (provided by Postfix)
- With aliases, you can send e-mail from AND receive e-mail on multiple e-mail addresses which are linked to the same inbox.
- This way, you can have multiple owners of one e-mail address.
- Forwarding (provided by Dovecot Sieve)
- With forwarding, you can only receive e-mail sent to one address on multiple e-mail addresses.
- Dovecot Sieve allows you to set this up for all e-mails, but it can also restrict forwarding to e-mails which match certain characteristics, such as a specific “From” domain or e-mail address or with a specific e-mail subject.
- Server-side filtering using the Sieve (RFC5228) standard, which can be managed using the ManageSieve protocol (RFC5804) in mail clients such as Roundcube. Forwarding to multiple e-mail addresses.
- Apart from forwarding, Sieve also offers functionality such as automated archiving and flagging and sending notifications.
- Send-only / no reply e-mail addresses, convenient to use with e.g. software which needs to be able to send transactional e-mail but which does not need to receive e-mail.
Prerequisites
This post assumes that you have already setup a basic Debian Bullseye install. It is recommended to use a separate VM, VPS or system to host the mail server on. It also assumes you have a static IP address available for which you can set the reverse DNS records.
Unbound DNS resolver
Install Unbound using these steps. It is also possible to point to another DNS resolver, but it will need to:
- Support DNSSEC
- Allow making a very large amount of queries
Doing this avoids using free services such as Cloudflare’s 1.1.1.1 or Googles 8.8.8.8 / 8.8.4.4. If you use these services, you will quickly hit their rate limits as Rspamd has to perform a lot of DNS queries to perform its analysis of messages.
Firewall settings
Allow connections on the following ports. When using IPv4 NAT, also forward connections to the following ports on the public IP to the mail server.
- 25/TCP SMTP
- 80/TCP HTTP
- 443/TCP HTTPS
- 443/UDP QUIC
- 143/TCP IMAP
- 993/TCP IMAPS
- 587/TCP SMTP Submission
- 4190/TCP Sieve
Opening port 993/TCP is strictly not needed, but still enabled to allow setting up mail accounts in Microsoft Outlook. Due to a bug in some versions of Outlook, this is otherwise impossible.
Install fail2ban
sudo apt install fail2ban
Install Nginx
sudo apt install curl gpg -y
curl -sS http://nginx.org/keys/nginx_signing.key | sudo apt-key add -
sudo tee -a /etc/apt/sources.list <<EOF
# Official nginx repositories
deb http://nginx.org/packages/debian/ bullseye nginx
#deb-src http://nginx.org/packages/debian/ bullseye nginx
EOF
sudo apt update
sudo apt install nginx -y
sudo sed -i "s/user nginx;/user www-data;/g" /etc/nginx/nginx.conf
sudo mkdir -p /var/www/acme
sudo nano /etc/nginx/conf.d/default.conf
Add to server {}
listen [::]:80;
and
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
sudo systemctl enable nginx
sudo systemctl restart nginx
sudo apt install apache2-utils
Letsencrypt TLS certificates
I’m currently writing an admin GUI for this setup in Go. Therefore, Lego will be used as the Letsencrypt client. This will make it easier to integrate things such as automatic certificate management, DNS updates and renewals.
Diffie-Hellman parameters
wget https://raw.githubusercontent.com/internetstandards/dhe_groups/master/ffdhe4096.pem
sudo mv ffdhe4096.pem /etc/ssl/
Lego
sudo apt install lego
sudo mkdir -p /etc/lego/
sudo chmod 600 /etc/lego/
Use lego --help
for more information on available options.
If you want to use HTTP validation, use the following command for the main domain:
sudo lego -d mail.example.org -d imap.example.org -d smtp.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
sudo lego -d rspamd.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
sudo lego -d mta-sts.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
sudo lego -d autoconfig.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
Make sure to also request certificates for the mta-sts and autoconfig subdomain for other domains you use this mailserver for
sudo lego -d mta-sts.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
sudo lego -d autoconfig.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.webroot /var/www/acme --pem run
Alternatively, for DNS validation, e.g., using Cloudflare.
sudo CLOUDFLARE_EMAIL=cloudflare@example.org CLOUDFLARE_API_KEY=REPLACE_WITH_YOUR_API_KEY lego -d mail.example.org -d imap.example.org -d smtp.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --dns cloudflare --pem run
sudo CLOUDFLARE_EMAIL=cloudflare@example.org CLOUDFLARE_API_KEY=REPLACE_WITH_YOUR_API_KEY lego -d rspamd.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --dns cloudflare --pem run
DANE TLSA records
sudo nano /usr/local/bin/lego-extract-tlsa.sh
#! /bin/sh
printf '_25._tcp.%s. IN TLSA 3 1 1 %s\n' mail.example.org $(openssl x509 -in /etc/lego/certificates/mail.example.org.crt -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | hexdump -ve '/1 "%02x"')
exit
sudo chmod +x /usr/local/bin/lego-extract-tlsa.sh
sudo lego-extract-tlsa.sh
Add the output to your DNS settings. After doing this, verify this has been succesful by executing:
dig +dnssec +noall +answer +multi _25._tcp.mail.example.org. TLSA
Additionally, you may make use of the following online tools:
- https://dane.sys4.de/ (Specifically aimed at SMTP)
- https://check.sidnlabs.nl/dane/ (Only suitable for HTTPS)
Set up renewals
sudo nano /usr/local/bin/lego-renew-hook-mail.sh
#! /bin/sh
/bin/systemctl restart dovecot nginx postfix
exit
sudo chmod +x /usr/local/bin/lego-renew-hook-mail.sh
sudo nano /usr/local/bin/lego-renew-hook.sh
#! /bin/sh
/bin/systemctl restart nginx
exit
sudo chmod +x /usr/local/bin/lego-renew-hook.sh
Option A: Install in crontab
This section assumes you have used HTTP validation in the previous steps. If you chose an alternative approach, alter these config settings as required.
sudo apt install cron -y
sudo crontab -e
## Mailserver domains mail.example.org, smtp.example.org, imap.example.org
@weekly /usr/local/bin/lego -d="mail.example.org" -d="imap.example.org" -d="smtp.example.org" -a -m="letsencrypt@example.org" -k="rsa4096" --path="/etc/lego/" --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook-mail.sh
## Rspamd
@weekly /usr/local/bin/lego -d rspamd.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
## MTA-STS
### Main domain
@weekly /usr/local/bin/lego -d mta-sts.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
### Virtual domains
@weekly /usr/local/bin/lego -d mta-sts.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
## Autoconfig
### Main domain
@weekly /usr/local/bin/lego -d autoconfig.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
### Virtual domains
@weekly /usr/local/bin/lego -d autoconfig.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
Option B: Use Systemd Timers
sudo nano /usr/local/bin/renew-certificates.sh
#!/bin/sh
## Mailserver domains mail.example.org, smtp.example.org, imap.example.org
/usr/local/bin/lego -d="mail.example.org" -d="imap.example.org" -d="smtp.example.org" -a -m="letsencrypt@example.org" -k="rsa4096" --path="/etc/lego/" --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook-mail.sh
## Rspamd
/usr/local/bin/lego -d rspamd.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
## MTA-STS
### Main domain
/usr/local/bin/lego -d mta-sts.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
### Virtual domains
/usr/local/bin/lego -d mta-sts.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
## Autoconfig
### Main domain
/usr/local/bin/lego -d autoconfig.example.org -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
### Virtual domains
/usr/local/bin/lego -d autoconfig.example.com -a -m letsencrypt@example.org -k rsa4096 --path /etc/lego --http --http.port 443 --http.webroot /var/www/acme --pem renew --reuse-key --renew-hook /usr/local/bin/lego-renew-hook.sh
exit
sudo chmod +x /usr/local/bin/renew-certificates.sh
sudo systemctl edit --full --force renew-certificates.service
```conf
[Unit]
Description=Renew Letsencrypt certificates
[Service]
Type=oneshot
ExecStart=/usr/local/bin/renew-certificates.sh
sudo systemctl edit --full --force renew-certificates.timer
[Unit]
Description=Weekly renewal of Letsencrypt certificates
[Timer]
OnCalendar=weekly
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl enable renew-certificates.timer --now
PostgreSQL Server
Perform the following commands on the server running your postgresql instance. In this example, PostgreSQL runs on host 10.10.10.1
.
You may opt to run PostgreSQL locally by executing sudo apt install postgresql -y
.
On the PostgreSQL host, execute:
sudo -u postgres psql
CREATE DATABASE vmail
ENCODING 'utf8';
CREATE USER vmail WITH password '____REPLACE_WITH_A_PASSWORD____';
GRANT ALL privileges ON DATABASE vmail TO vmail;
\q
OR
sudo -i -u postgres psql -c "CREATE DATABASE vmail ENCODING 'utf8';"
sudo -i -u postgres psql -c "CREATE USER vmail WITH password '____REPLACE_WITH_A_PASSWORD____';"
sudo -i -u postgres psql -c "GRANT ALL privileges ON DATABASE vmail TO vmail;"
On the mail server, execute:
sudo apt install -y postgresql-client
psql -h 10.10.10.1 -d vmail -U vmail -W
CREATE TABLE domains (
id serial PRIMARY KEY,
domain varchar(255) UNIQUE NOT NULL
);
CREATE TABLE accounts (
id serial PRIMARY KEY,
username varchar(64) NOT NULL,
domain varchar(255) NOT NULL,
password varchar(255) NOT NULL,
quota numeric CHECK(quota >= 0) DEFAULT '0',
enabled boolean DEFAULT '0',
sendonly boolean DEFAULT '0',
UNIQUE (username, domain),
FOREIGN KEY (domain) REFERENCES domains (domain)
);
CREATE TABLE aliases (
id serial PRIMARY KEY,
source_username varchar(64),
source_domain varchar(255) NOT NULL,
destination_username varchar(64) NOT NULL,
destination_domain varchar(255) NOT NULL,
enabled boolean DEFAULT '0',
UNIQUE (source_username, source_domain, destination_username, destination_domain),
FOREIGN KEY (source_domain) REFERENCES domains (domain)
);
CREATE TYPE policy AS ENUM ('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure');
CREATE TABLE tlspolicies (
id serial PRIMARY KEY,
domain varchar(255) UNIQUE NOT NULL,
policy policy NOT NULL,
params varchar(255)
);
\q
Add vmail user
sudo mkdir /var/vmail
sudo addgroup --gid 5000 vmail
sudo adduser --disabled-login --system --disabled-password --home /var/vmail --gecos "Virtual Mail User" --uid 5000 --gid 5000 --shell /usr/sbin/nologin vmail
sudo mkdir /var/vmail/mailboxes
sudo mkdir -p /var/vmail/sieve/global
sudo chown -R vmail /var/vmail
sudo chgrp -R vmail /var/vmail
sudo chmod -R 770 /var/vmail
Install Dovecot
sudo apt install swaks
sudo apt install dovecot-core dovecot-imapd dovecot-lmtpd dovecot-pgsql dovecot-sieve dovecot-managesieved
Configure Dovecot
sudo mkdir /etc/dovecot.old
sudo mv /etc/dovecot/* /etc/dovecot.old/
cd /etc/dovecot
sudo nano /etc/dovecot/dovecot.conf
# Protocols
protocols = imap lmtp sieve
# Plugins
mail_plugins =
# TLS configuration
#
# generated 2022-01-11, Mozilla Guideline v5.6, Dovecot 2.3.9, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.9&config=intermediate&openssl=1.1.1d&guideline=5.6
ssl = required
ssl_cert = </etc/lego/certificates/mail.example.org.crt
ssl_key = </etc/lego/certificates/mail.example.org.key
ssl_dh = </etc/ssl/ffdhe4096.pem
ssl_min_protocol = TLSv1.2
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_prefer_server_ciphers = no
# Dovecot services
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
}
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
group = postfix
user = postfix
}
user = vmail
}
service auth {
## Auth socket for Postfix
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
## Auth socket for LMTP service
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
service stats {
fifo_listener stats-mail {
user = vmail
mode = 0644
}
inet_listener {
address = 127.0.0.1
port = 24242
}
}
# Protocol settings
protocol imap {
mail_plugins = $mail_plugins quota imap_quota imap_sieve
mail_max_userip_connections = 20
imap_idle_notify_interval = 29 mins
}
protocol lmtp {
postmaster_address = postmaster@example.org
mail_plugins = $mail_plugins sieve notify push_notification
}
# Client authentication
disable_plaintext_auth = yes
auth_mechanisms = plain login
auth_username_format = %Lu
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf
}
userdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf
}
# Delimiter for "address tagging"
recipient_delimiter = +
# Mail location
mail_uid = vmail
mail_gid = vmail
mail_privileged_group = vmail
mail_home = /var/vmail/mailboxes/%d/%n
mail_location = maildir:~/mail:LAYOUT=fs
# Mailbox configuration
namespace inbox {
inbox = yes
mailbox Spam {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Sent {
auto = subscribe
special_use = \Sent
}
}
# Mail plugins
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_before = /var/vmail/sieve/global/spam-global.sieve
sieve = file:/var/vmail/sieve/%d/%n/scripts;active=/var/vmail/sieve/%d/%n/active-script.sieve
## Spam learning
### From elsewhere to Spam folder
imapsieve_mailbox1_name = Spam
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/var/vmail/sieve/global/learn-spam.sieve
### From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Spam
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/var/vmail/sieve/global/learn-ham.sieve
sieve_pipe_bin_dir = /usr/local/bin/dovecot/sieve
sieve_global_extensions = +vnd.dovecot.pipe
quota = maildir:User quota
quota_exceeded_message = User %u has exhausted allowed storage space.
## Stats
### how often to session statistics (must be set)
stats_refresh = 30 secs
### track per-IMAP command statistics (optional)
stats_track_cmds = yes
}
sudo nano /etc/dovecot/dovecot-sql.conf
In this config example, alter the connect parameters to match your setup.
driver=pgsql
connect = "host=10.10.10.1 dbname=vmail user=vmail password=____PASSWORD____"
default_pass_scheme = ARGON2ID
password_query = SELECT username AS user, domain, password FROM accounts WHERE username = '%Ln' AND domain = '%Ld' and enabled = true;
user_query = SELECT CONCAT('*:storage=', quota, 'M') AS quota_rule FROM accounts WHERE username = '%Ln' AND domain = '%Ld' AND sendonly = false;
# For using doveadm -A
iterate_query = SELECT username, domain FROM accounts WHERE sendonly = false;
sudo chmod 440 /etc/dovecot/dovecot-sql.conf
Global spam filter
sudo mkdir -p /usr/local/bin/dovecot/sieve
sudo nano /usr/local/bin/dovecot/sieve/rspamd-learn-spam.sh
#!/bin/sh
exec /usr/bin/rspamc -h /run/rspamd/worker-controller.socket learn_spam
sudo nano /usr/local/bin/dovecot/sieve/rspamd-learn-ham.sh
#!/bin/sh
exec /usr/bin/rspamc -h /run/rspamd/worker-controller.socket learn_ham
sudo chmod +x /usr/local/bin/dovecot/sieve/rspamd-learn-ham.sh
sudo chmod +x /usr/local/bin/dovecot/sieve/rspamd-learn-spam.sh
sudo nano /var/vmail/sieve/global/spam-global.sieve
require "fileinto";
if header :contains "X-Spam-Flag" "YES" {
fileinto "Spam";
}
if header :is "X-Spam" "Yes" {
fileinto "Spam";
}
sudo nano /var/vmail/sieve/global/learn-spam.sieve
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "rspamd-learn-spam.sh" [ "${username}" ];
sudo nano /var/vmail/sieve/global/learn-ham.sieve
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "${1}";
}
if string "${mailbox}" "Trash" {
stop;
}
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "rspamd-learn-ham.sh" [ "${username}" ];
Install Postfix
sudo apt install postfix postfix-pgsql rsyslog postfix-mta-sts-resolver
When prompted, about the Postfix configuration, choose “no configuration”.
Configure Postfix
sudo systemctl stop postfix
sudo nano /etc/postfix/main.cf
# Network settings
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
inet_interfaces = all
inet_protocols = all
myhostname = mail.example.org
## If you run a Postfix server behind a proxy or NAT, you need to configure the proxy_interfaces parameter and specify all the external proxy or NAT addresses that Postfix receives mail on.
## In other words, if you use port forwarding from public IP addresses to a private IP address, enter those public IP addresses below.
proxy_interfaces =
# Mail queue settings
maximal_queue_lifetime = 1h
bounce_queue_lifetime = 1h
maximal_backoff_time = 15m
minimal_backoff_time = 5m
queue_run_delay = 5m
# TLS settings
## Do not allow compression or client initiated renegotiation
tls_ssl_options = NO_COMPRESSION NO_RENEGOTIATION
## Enforce server enforced cipher preference
tls_preempt_cipherlist = yes
# Outbound SMTP connections (Postfix as sender / client)
smtp_tls_security_level = dane
smtp_dns_support_level = dnssec
## With this smtp_tls_policy_maps setting, Postfix first checks for policies in the database. If these are not available, the mta-sts policy resolver is contacted.
smtp_tls_policy_maps = pgsql:/etc/postfix/sql/tls-policy.cf, socketmap:inet:127.0.0.1:8461:postfix
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_ciphers = high
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
# Inbound SMTP connections (Postfix as receiver / server)
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_ciphers = high
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA
smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA
smtpd_tls_dh1024_param_file = /etc/ssl/ffdhe4096.pem
smtpd_tls_cert_file = /etc/lego/certificates/mail.example.org.crt
smtpd_tls_key_file = /etc/lego/certificates/mail.example.org.key
# Local mail delivery to Dovecot via LMTP
virtual_transport = lmtp:unix:private/dovecot-lmtp
# Spam filter and DKIM signatures via Rspamd
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
milter_protocol = 6
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
milter_default_action = accept
# Server Restrictions for clients, recipients and relaying
## (concerning S2S-connections. Mailclient-connections are configured in submission-section in master.cf)
## Conditions in which Postfix works as a relay. (for mail user clients)
smtpd_relay_restrictions = reject_non_fqdn_recipient
reject_unknown_recipient_domain
permit_mynetworks
reject_unauth_destination
## Conditions in which Postfix accepts e-mails as recipient (additional to relay conditions)
## check_recipient_access checks if an account is "sendonly" and rejects mails if this is the case, unless the sendonly recipient is also in aliases.
smtpd_recipient_restrictions = check_recipient_access pgsql:/etc/postfix/sql/recipient-access.cf
## Restrictions for all sending foreign servers ("SMTP clients")
smtpd_client_restrictions = permit_mynetworks
check_client_access hash:/etc/postfix/without_ptr
reject_unknown_client_hostname
## Foreign mail servers must present a valid "HELO"
smtpd_helo_required = yes
smtpd_helo_restrictions = permit_mynetworks
reject_invalid_helo_hostname
reject_non_fqdn_helo_hostname
reject_unknown_helo_hostname
## Block clients, which start sending too early
smtpd_data_restrictions = reject_unauth_pipelining
## Restrictions for MUAs (Mail user agents)
mua_relay_restrictions = reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_mynetworks,permit_sasl_authenticated,reject
mua_sender_restrictions = permit_mynetworks,reject_non_fqdn_sender,reject_sender_login_mismatch,permit_sasl_authenticated,reject
mua_client_restrictions = permit_mynetworks,permit_sasl_authenticated,reject
# Postscreen Filter
## Postscreen Whitelist / Blocklist
postscreen_access_list = permit_mynetworks
cidr:/etc/postfix/postscreen_access
postscreen_blacklist_action = drop
## Drop connections if other server is sending too quickly
postscreen_greet_action = drop
## DNS blocklists
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites = ix.dnsbl.manitu.net*2
zen.spamhaus.org*2
postscreen_dnsbl_action = drop
# PostgreSQL queries
virtual_alias_maps = pgsql:/etc/postfix/sql/aliases.cf
virtual_mailbox_maps = pgsql:/etc/postfix/sql/accounts.cf
virtual_mailbox_domains = pgsql:/etc/postfix/sql/domains.cf
local_recipient_maps = $virtual_mailbox_maps
# Miscellaneous
## Maximum mailbox size (0=unlimited - is already limited by Dovecot quota)
mailbox_size_limit = 0
## Maximum size of inbound e-mails (50 MB)
message_size_limit = 52428800
## Do not notify system users on new e-mail
biff = no
## Users always have to provide full e-mail addresses
append_dot_mydomain = no
## Delimiter for "address tagging"
recipient_delimiter = +
## Disable the VRFY command to prevent leaking valid addresses to scanners
disable_vrfy_command = yes
## Disable backwards compatible default settings
### (http://www.postfix.org/COMPATIBILITY_README.html)
compatibility_level = 2
Make sure to customise myhostname
, smtpd_tls_cert_file
, smtpd_tls_key_file
and proxy_interfaces
(if necessary) in main.cf
.
sudo nano /etc/postfix/master.cf
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
smtp inet n - y - 1 postscreen
-o smtpd_sasl_auth_enable=no
smtpd pass - - y - - smtpd
dnsblog unix - - y - 0 dnsblog
tlsproxy unix - - y - 0 tlsproxy
submission inet n - y - - 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_sasl_security_options=noanonymous
-o smtpd_client_restrictions=$mua_client_restrictions
-o smtpd_sender_restrictions=$mua_sender_restrictions
-o smtpd_relay_restrictions=$mua_relay_restrictions
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_sender_login_maps=pgsql:/etc/postfix/sql/sender-login-maps.cf
-o smtpd_helo_required=no
-o smtpd_helo_restrictions=
-o cleanup_service_name=submission-header-cleanup
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - y 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
defer unix - - y - 0 bounce
trace unix - - y - 0 bounce
verify unix - - y - 1 verify
flush unix n - y 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - y - - smtp
relay unix - - y - - smtp
showq unix n - y - - showq
error unix - - y - - error
retry unix - - y - - error
discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil
scache unix - - y - 1 scache
submission-header-cleanup unix n - n - 0 cleanup
-o header_checks=regexp:/etc/postfix/submission_header_cleanup
sudo nano /etc/postfix/submission_header_cleanup
In order to improve sender privacy, we tell Postfix to clean up the headers which some Mail User Agents (MUAs) such as Mozilla Thunderbird, Apple Mail.app or Microsoft Outlook include from messages that are sent through this server. It is not necessary for a recipient of our messages to know which software vendors and versions are being used to compose and send messages.
### Removes headers of MUAs for privacy reasons
/^Received:/ IGNORE
/^X-Originating-IP:/ IGNORE
/^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE
sudo mkdir /etc/postfix/sql
cd /etc/postfix/sql/
In the following config examples, alter the database login parameters to match your setup.
sudo nano accounts.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
query = SELECT 1 AS found FROM accounts WHERE username = '%u' AND domain = '%d' AND enabled = true LIMIT 1;
sudo nano aliases.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
query = SELECT CONCAT(destination_username, '@', destination_domain) AS destinations FROM aliases WHERE source_username = '%u' AND source_domain = '%d' AND enabled = true;
sudo nano domains.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
query = SELECT domain FROM domains WHERE domain='%s';
sudo nano recipient-access.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
# Without aliases exception:
# query = SELECT CASE WHEN (sendonly = true) THEN 'REJECT' ELSE 'OK' END AS access FROM accounts WHERE username = '%u' AND domain = '%d' AND enabled = true LIMIT 1;
# With aliases exception
query = SELECT CASE WHEN (accounts.sendonly = true AND NOT EXISTS (SELECT FROM aliases WHERE aliases.source_username = '%u' AND aliases.source_domain = '%d' AND aliases.enabled = true)) THEN 'REJECT' ELSE 'OK' END AS access FROM accounts WHERE accounts.username = '%u' AND accounts.domain = '%d' AND accounts.enabled = true LIMIT 1;
sudo nano sender-login-maps.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
query = SELECT CONCAT(username, '@', domain) AS owns FROM accounts WHERE username = '%u' AND domain = '%d' AND enabled = true UNION SELECT CONCAT(destination_username, '@', destination_domain) AS owns FROM aliases WHERE source_username = '%u' AND source_domain = '%d' AND enabled = true;
sudo nano tls-policy.cf
user = vmail
password = ____PASSWORD____
hosts = 10.10.10.1
dbname = vmail
query = SELECT policy, params FROM tlspolicies WHERE domain = '%s';
sudo chmod -R 640 /etc/postfix/sql
cd
sudo touch /etc/postfix/without_ptr
sudo touch /etc/postfix/postscreen_access
sudo postmap /etc/postfix/without_ptr
Install Rspamd & ClamAV
Rspamd
sudo apt install -y lsb-release wget
wget -O- https://rspamd.com/apt-stable/gpg.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/rspamd.gpg >> /dev/null
sudo tee -a /etc/apt/sources.list <<EOF >> /dev/null
# Rspamd repository
deb http://rspamd.com/apt-stable/ $(lsb_release -c -s) main
#deb-src http://rspamd.com/apt-stable/ $(lsb_release -c -s) main
EOF
sudo apt update
sudo apt install rspamd
sudo systemctl stop rspamd
Redis
Redis is required as a storage for non-volatile data for Rspamd.
sudo apt install redis-server -y
sudo nano /etc/redis/redis.conf
We configure it to limit its maximum memory use and to not listen on TCP, instead we opt for a UNIX socket for increased performance by altering the following settings:
port 0
unixsocket /run/redis/redis-server.sock
unixsocketperm 770
maxmemory 500mb
Add the _rspamd
user to the redis
group to allow writing and reading from the redis unix socket.
sudo usermod -a -G redis _rspamd
sudo systemctl restart redis
Configure Rspamd
Run the configuration wizard
sudo rspamadm configwizard
Answer the questions as follows
- Controller password is not set, do you want to set one?[Y/n]:
y
- Do you wish to set Redis servers?[Y/n]:
y
- Input read only servers separated by
,
[default: localhost]:/run/redis/redis-server.sock
- Input write only servers separated by
,
[default: localhost]:/run/redis/redis-server.sock
- Do you have any password set for your Redis?[y/N]:
n
- Do you have any specific database for your Redis?[y/N]:
n
- Do you want to setup dkim signing feature?[y/N]:
y
- How would you like to set up DKIM signing?
- Use domain from mime from header for sign
- Use domain from SMTP envelope from for sign
- Use domain from authenticated user for sign
- Sign all mail from specific networks
- Enter your choice (1, 2, 3, 4) [default: 1]: 1
- Do you want to sign mail from authenticated users? [Y/n]:
y
- Allow data mismatch, e.g. if mime from domain is not equal to authenticated user domain? [Y/n]:
y
- Do you want to use effective domain (e.g. example.com instead of foo.example.com)? [Y/n]:
y
- Enter output directory for the keys [default: /var/lib/rspamd/dkim/]:
/var/lib/rspamd/dkim
- Enter domain to sign: example.org
- Enter selector [default: dkim]: 2021
- Do you want to create privkey /var/lib/rspamd/dkim/example.org.2020.key[Y/n]:
y
- Do you wish to add another DKIM domain?[y/N]:
choose applicable option
NB: Generate a DKIM key for every domain that is to be used with this mail server. This can also be done later, e.g., after setting up or when adding new domains. Do not create keys for subdomains. As described in the Rspamd Documentation: “The default global config (fallback mode) searches for keys at the defined path. The path is constructed using the eSLD normalized domain name of header from and the default selector defined with selector (dkim). So the search path for user@test.example.com
would be /var/lib/rspamd/dkim/example.com.dkim.key
. If a key is found the message will be signed.”
Enable ARC signatures
The configuration of the ARC module is very similar to the DKIM module, so we will make them share the same config file.
sudo ln -s /etc/rspamd/local.d/dkim_signing.conf /etc/rspamd/local.d/arc.conf
Rspamd logging
The following override can be used to debug specific Rspamd modules.
sudo nano /etc/rspamd/local.d/logging.inc
# Enable debug for specific modules (e.g. `debug_modules = ["dkim", "re_cache"];`)
debug_modules = [];
EOF
Spam headers
sudo tee -a /etc/rspamd/local.d/milter_headers.conf <<EOF >> /dev/null
use = ["x-spamd-bar", "x-spam-level", "authentication-results"];
authenticated_headers = ["authentication-results"];
extended_spam_headers = true;
EOF
Rspamd Clamav Antivirus integration
sudo tee /etc/rspamd/local.d/antivirus.conf <<EOF >> /dev/null
clamav {
symbol = "CLAM_VIRUS";
type = "clamav";
action = "reject";
servers = "av.example.org:3310";
patterns {
JUST_EICAR = "^Eicar-Test-Signature$";
}
whitelist = "/etc/rspamd/antivirus.wl";
}
EOF
Rspamd Bayes classifier
sudo tee /etc/rspamd/local.d/classifier-bayes.conf <<EOF >> /dev/null
backend = "redis";
autolearn = true;
EOF
Make the Rspamd web interface listen on a UNIX socket rather than a TCP socket
sudo tee -a /etc/rspamd/local.d/worker-controller.inc <<EOF >> /dev/null
bind_socket = "/run/rspamd/worker-controller.socket mode=0666 owner=_rspamd";
EOF
Enable Rspamd phishing protection
OpenPhish and PhishTank track known phishing URLs and publish these on free feeds. By enabling the Rspamd phishing module, these feeds are downloaded and used to filter out phishing attempts.
sudo nano /etc/rspamd/local.d/phishing.conf
phishing {
# Enable openphish support (default disabled)
openphish_enabled = true;
# URL of feed, default is public url:
openphish_map = "https://www.openphish.com/feed.txt";
# For premium feed, change that to your personal URL, e.g.
# openphish_map = "https://openphish.com/samples/premium_feed.json";
# Change this to true if premium feed is enabled
openphish_premium = false;
}
phishtank_enabled = true
Enable Pyzor integration
Pyzor is a bulk email scanner similar to Razor2 and DCC, which doesn’t identify spam but instead identifies how often a message (hash) has been seen or how “bulky” a message is.
sudo apt install pyzor
sudo nano /etc/rspamd/local.d/external_services.conf
pyzor {
# default pyzor settings
servers = "localhost:5953"
}
sudo systemctl edit --force --full pyzor.socket
[Unit]
Description=Pyzor socket
[Socket]
ListenStream=127.0.0.1:5953
ListenStream=[::1]:5953
Accept=yes
[Install]
WantedBy=sockets.target
sudo systemctl edit --force --full pyzor@.service
[Unit]
Description=Pyzor Socket Service
Requires=pyzor.socket
[Service]
Type=simple
ExecStart=-/usr/bin/pyzor check
StandardInput=socket
StandardError=journal
TimeoutStopSec=10
User=_rspamd
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
sudo -u _rspamd mkdir /var/lib/rspamd/.pyzor
sudo systemctl enable pyzor.socket --now
Enable Razor integration
Razor is, like Pyzor and DCC, a bulk email scanner that doesn’t detect spam but how often a message (hash) has been seen, or how “bulky” a message is.
sudo nano /etc/rspamd/local.d/external_services.conf
razor {
# default razor settings
servers = "localhost:11342"
}
sudo systemctl edit --force --full razor.socket
[Unit]
Description=Razor socket
[Socket]
ListenStream=127.0.0.1:11342
ListenStream=[::1]:11342
Accept=yes
[Install]
WantedBy=sockets.target
sudo systemctl edit --force --full razor@.service
[Unit]
Description=Razor Socket Service
Requires=razor.socket
[Service]
Type=simple
ExecStart=/bin/sh -c '/usr/bin/razor-check && /usr/bin/echo -n "spam" || /usr/bin/echo -n "ham"'
StandardInput=socket
StandardError=journal
TimeoutStopSec=10
User=_rspamd
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
sudo systemctl enable razor.socket --now
Send DMARC reports to senders
sudo nano /etc/rspamd/local.d/dmarc.conf
reporting {
# Required attributes
enabled = true; # Enable reports in general
email = 'dmarc-reports@example.org'; # Source of DMARC reports
domain = 'example.org'; # Domain to serve
org_name = 'Example'; # Organisation
# Optional parameters
# bcc_addrs = ["postmaster@example.com"]; # additional addresses to copy on reports
# report_local_controller = false; # Store reports for local/controller scans (for testing only)
helo = 'rspamd.localhost'; # Helo used in SMTP dialog
smtp = '127.0.0.1'; # SMTP server IP
smtp_port = 25; # SMTP server port
from_name = 'Rspamd'; # SMTP FROM
msgid_from = 'rspamd'; # Msgid format
max_entries = 1k; # Maximum amount of entries per domain
keys_expire = 2d; # Expire date for Redis keys
#only_domains = '/path/to/map'; # Only store reports from domains or eSLDs listed in this map
# Available from 3.3
#exclude_domains = '/path/to/map'; # Exclude reports from domains or eSLDs listed in this map
#exclude_domains = ["example.com", "another.com"]; # Alternative, use array to exclude reports from domains or eSLDs
# Available from 3.8
#exclude_recipients = '/path/to/map'; # Exclude reports for recipients listed in this map
#exclude_recipients = ["a@example.com", "b@another.com"]; # Alternative, use array to exclude reports for recipients
}
sudo systemctl edit --force --full rspamd-dmarc-report.service
[Unit]
Description=Send DMARC Reports with Rspamd
Requires=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=_rspamd
Group=_rspamd
WorkingDirectory=/var/lib/rspamd
ExecStart=/bin/sh -c '/usr/bin/rspamadm dmarc_report -v $$(date -u -d "yesterday" "+%Y-%m-%d")'
# Security directives
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
sudo systemctl edit --full --force rspamd-dmarc-report.timer
[Unit]
Description=Timer for Daily DMARC Reports with Rspamd
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
The Persistent=true
option ensures that the timer will catch up if it missed any runs while the system was down.
sudo systemctl enable rspamd-dmarc-report.timer --now
Restart Rspamd
sudo systemctl restart rspamd
Configure Nginx as Rspamd reverse proxy
printf REPLACE_WITH_YOUR_RSPAMD_PASSWORD | sudo htpasswd -ic /etc/nginx/rspamd.htpasswd management
sudo nano /etc/nginx/conf.d/rspamd.conf
server {
listen 80;
listen [::]:80;
server_name rspamd.example.org;
location / {
return 301 https://$server_name$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name rspamd.example.org;
ssl_certificate /etc/lego/certificates/rspamd.example.org.crt;
ssl_certificate_key /etc/lego/certificates/rspamd.example.org.key;
ssl_dhparam /etc/ssl/ffdhe4096.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
resolver 10.10.10.1 [2001:db8::2] valid=300s;
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=63072000; preload";
keepalive_timeout 300s;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
# Rspamd
location / {
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/rspamd.htpasswd;
proxy_pass http://unix:/run/rspamd/worker-controller.socket:/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Optional: generate extra DKIM keys
sudo rspamadm dkim_keygen -d example.org -s 2023
Generate a DKIM key for every domain that is to be used with this mail server. Do not create keys for subdomains. As described in the Rspamd Documentation: “The default global config (fallback mode) searches for keys at the defined path. The path is constructed using the eSLD normalized domain name (-d
) of header from and the default selector (-s
) defined with selector (dkim). So the search path for user@test.example.com
would be /var/lib/rspamd/dkim/example.com.dkim.key
. If a key is found the message will be signed.”
Afterwards, ensure the filesystem permissions are still in good order
sudo chown -R _rspamd:_rspamd /var/lib/rspamd
Then, add each domain to /etc/rspamd/local.d/dkim_signing.conf
sudo nano /etc/rspamd/local.d/dkim_signing.conf
domain {
example.org {
path = "/var/lib/rspamd/dkim/example.org.2023.key";
selector = "2023";
}
example.com {
path = "/var/lib/rspamd/dkim/example.com.2019.key";
selector = "2019";
}
}
Alternatively, to avoid this hassle, use the same DKIM key for every domain on the server.
sudo rspamadm dkim_keygen -b 2048 -s 2023 -k /var/lib/rspamd/dkim/2023.key > /var/lib/rspamd/dkim/2023.key.pub
sudo chown -R _rspamd:_rspamd /var/lib/rspamd
sudo nano /etc/rspamd/local.d/dkim_signing.conf
path = "/var/lib/rspamd/dkim/$selector.key";
selector = "2023";
sudo systemctl restart rspamd
Set DNS records
Set the DKIM record in DNS
Add the DKIM record generated by Rspamd to the DNS records of your domain. To review the public DKIM keys that need to be added, execute
sudo ls /var/lib/rspamd/dkim
sudo cat /var/lib/rspamd/dkim/example.org.2020.key.pub
Set the other required DNS records as well
For the mail server
- A and AAAA records
mail.example.org
imap.example.org
smtp.example.org
autoconfig.example.org
rspamd.example.org
mta-sts.example.org
- Reverse DNS / PTR records
2001:db8::5 mail.example.org
203.0.113.205 mail.example.org
- Main _mta-sts TXT record
- format:
v=STSv1; id=202201111500Z
where id is the date on which you last changed the MTA-STS policy as YYYYMMDDHHMMTZ (year, month, day, hour, minute, timezone) - More details can be found in the MTA-STS section
- format:
- TLSA records
- Explained in section DANE TLSA records
For every domain which uses this mail server
- CNAME records
- autoconfig.example.com –> autoconfig.example.org
- _mta-sts.example.com. IN CNAME _mta-sts.example.org.
- mta-sts.example.com IN CNAME mta-sts.example.org.
- MX records
example.org mx 0 mail.example.org
- TXT records
- SPF records
- Some resources recommend a neutral record
?all
, because the concept of SPF is flawed. The Dutch Internet Standards Platform recommends against this, referencing official NCSC documentation and encourages a softfail~all
or hardfail-all
record. I personally chose compromise between the two opinions and use softfail. v=spf1 mx ~all
- Some resources recommend a neutral record
- DKIM record
- Get your records from
sudo cat /var/lib/rspamd/dkim/keyname.txt
, wherekeyname
is the name you chose while configuring Rspamd
- Get your records from
- DMARC records
_dmarc
TXTv=DMARC1; p=reject; rua=mailto:dmarc-reports@example.org; ruf=mailto:dmarc-reports@example.org; fo=1;
- First set the DMARC policy as
p=none
for testing purposes. After verifying for a while that this does not pose problems, change the DMARC policy top=reject
.
- TLSRPT record
_smtp._tls TXT v=TLSRPTv1; rua=mailto:tlsrpt@example.com
- SPF records
- SRV records. These are used by some e-mail clients to automatically configure the mail server settings when adding a new account.
_imap._tcp.example.org 0 1 143 mail.example.org
_submission._tcp.example.org 0 1 587 mail.example.org
_sieve._tcp.example.org 0 1 4190 mail.example.org
Setup domains, accounts & aliases
Generate password hashes for accounts
sudo doveadm pw -s ARGON2ID
Add domains, accounts & aliases to the database
sudo -u postgres psql
\c vmail;
INSERT INTO domains (domain) VALUES ('example.org');
INSERT INTO accounts (username, domain, password, quota, enabled, sendonly) VALUES ('it', 'example.org', '{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$4oLPvatR15+v8T7XRUsRLQ$CQHMhBYh0cZRq6agxAg7j/nqmn+z3bNLdgkNNhXzN1s', 0, true, false);
Standard aliases query
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('postmaster', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('abuse', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('noc', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('security', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('hostmaster', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('webmaster', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('www', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('dmarc-reports', 'example.org', 'it', 'example.org', true);
INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) values ('tlsrpt', 'example.org', 'it', 'example.org', true);
Restart all
sudo systemctl restart dovecot
sudo systemctl restart postfix
sudo systemctl restart rspamd
sudo systemctl restart nginx
Reboot
sudo reboot
Improving deliverability
Get notified
In order to improve deliverability and get early notices about possible spam mail being sent from our mail server, it is advisable to sign up to the following platforms.
- Microsoft Smart Network Data Services (SNDS)
- Go to “Request Access” and enter public IPv4 address, e.g. 203.0.113.1/32.
- Microsoft Junk Mail Reporting Program (JMRP)
- DNSWL
- Receive DMARC reports via the Return Path Feedback Loop Service
- Google Postmaster Tools
Be careful
Be careful about what you send to others and follow best practices.
Resolving deliverability issues
Whether or not the e-mails you send end up in recipient’s inboxes instead of their junk folders depends, amongst other factors, on the reputation of the IP address of your mail server. When hosting your mail server on shared infrastructure you may suffer from ’noisy neighbours’, especially when the number of e-mails that are sent out through your mail server are low. If you encounter issues with deliverability to e-mail addresses hosted by specific providers, the following starting points may help in resolving these.
Microsoft: Outlook.com, Hotmail, Live, MSN
Make sure to follow Microsoft’s Postmaster Policies, Practices, and Guidelines. If deliverability issues persist, request support through Outlook.com Deliverability Support.
Microsoft: Office 365
If your e-mail address or IP is being blocked by Microsoft Office 365, contact the Office 365 Anti-Spam IP Delist Portal.
Apple: mac.com, me.com, iCloud.com
When Apple refuses e-mail from your server, refer to their Postmaster information for iCloud Mail and make sure the best practices they describe are being followed. Contact Apple using the provided contact details if necessary.
Using an SMTP relay service
Services such as Twilio Sendgrid and Amazon Simple Email Service (SES) allow you to outsource ensuring deliverability. However, using them also means giving up some privacy, as these services will scan any outgoing e-mail going through them.
At the moment of writing, Sendgrid is free to use for up to 100 messages per day and charges $14.95 for sending up to 50,000 messages per month. Amazon SES charges “$0.10 for every 1,000 emails you send” from a server outside of Amazon Web Services (AWS). When using Amazon EC2, you will be charged “$0 for the first 62,000 emails you send each month, and $0.10 for every 1,000 emails you send after that.” in the Europe (Frankfurt) region. In this section, this AWS region is used in all examples and links.
NB: Make sure to use a Chromium-based browser when using the AWS console, it is not entirely compatible and will behave strangely in Mozilla Firefox.
With both services, you will need to prove ownership of your domains. In Sendgrid, this can be done in the dashboard on the Sender Authentication page. On the AWS Console, this is done by Verifying identities. After validating each individual domain, add the following configuration to Postfix:
sudo nano /etc/postfix/relay_maps
@example.com [smtp.sendgrid.net]:587
@example.org [email-smtp.eu-central-1.amazonaws.com]:587
sudo nano /etc/postfix/sasl_passwd
In this file, paste the login details of the SMTP relayhost services. For AWS, a user can be created by following these instructions. For Sendgrid, an API key is needed which can be generated on this page.
[smtp.sendgrid.net]:587 apikey:____YOUR_PASSPHRASE____
[email-smtp.eu-central-1.amazonaws.com]:587 ___YOUR_USERNAME___:____YOUR_PASSPHRASE____
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/relay_maps
sudo postmap /etc/postfix/sasl_passwd
sudo nano /etc/postfix/main.cf
Add above # Outbound SMTP connections (Postfix as sender / client)
:
# Domain-based outgoing email relay policy
sender_dependent_relayhost_maps = hash:/etc/postfix/relay_maps
# SASL authentication settings for relayhosts
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
header_size_limit = 4096000
sudo systemctl restart postfix
Mozilla Thunderbird Autoconfig
By default, e-mail software will try to guess the location of a domain’s mail server and the protocols it speaks. This is error-prone and often slow. Thunderbird and many other e-mail clients will try to retrieve autoconfig.example.org
to prevent having to guess.
sudo mkdir -p /var/www/autoconfig/mail/
sudo nano /var/www/autoconfig/mail/config-v1.1.xml
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="example.org">
<domain>example.org</domain>
<displayName>Example.org Mail Server</displayName>
<displayShortName>Organisation example</displayShortName>
<incomingServer type="imap">
<hostname>mail.example.org</hostname>
<port>143</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>mail.example.org</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url="https://example.org/">
<descr lang="en">Homepage</descr>
</documentation>
</emailProvider>
</clientConfig>
sudo nano /etc/nginx/conf.d/autoconfig.conf
server {
listen 80;
listen [::]:80;
server_name autoconfig.example.org;
location / {
return 301 https://$server_name$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name autoconfig.example.org;
ssl_certificate /etc/lego/certificates/autoconfig.example.org.crt;
ssl_certificate_key /etc/lego/certificates/autoconfig.example.org.key;
ssl_dhparam /etc/ssl/ffdhe4096.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
resolver 10.10.10.1 [2001:db8::2] valid=300s;
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=63072000; preload";
keepalive_timeout 300s;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
root /var/www/autoconfig;
}
}
MTA-STS
DNS Records
- TLSRPT DNS TXT
_smtp._tls TXT v=TLSRPTv1; rua=mailto:tlsrpt@example.org
- MTA-STS DNS TXT
_mta-sts TXT v=STSv1; id=202012141200Z
- mta-sts A and AAAA or CNAME
Modes are: none, testing and enforcing. In the RFC8461 specification, we can read that these modes entail the following:
none
: In this mode, Sending MTAs should treat the Policy Domain as though it does not have any active policyenforce
: In this mode, Sending MTAs MUST NOT deliver the message to hosts that fail MX matching or certificate validation or that do not support STARTTLS.testing
: In this mode, Sending MTAs that also implement the TLSRPT (TLS Reporting) specification [RFC8460] send a report indicating policy application failures (as long as TLSRPT is also implemented by the recipient domain); in any case, messages may be delivered as though there were no MTA-STS validation failure.
sudo mkdir -p /var/www/mta-sts/.well-known
sudo tee /var/www/mta-sts/.well-known/mta-sts.txt <<EOF >> /dev/null
version: STSv1
mode: enforce
max_age: 10368000
mx: mail.example.org
EOF
sudo nano /etc/nginx/conf.d/mta-sts.conf
server {
listen 80;
listen [::]:80;
server_name mta-sts.example.org;
location / {
return 301 https://$server_name$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mta-sts.example.org;
ssl_certificate /etc/lego/certificates/mta-sts.example.org.crt;
ssl_certificate_key /etc/lego/certificates/mta-sts.example.org.key;
ssl_dhparam /etc/ssl/ffdhe4096.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
resolver 10.10.10.1 [2001:db8::2] valid=300s;
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=63072000; preload";
keepalive_timeout 300s;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
root /var/www/mta-sts;
}
}
sudo nginx -t
sudo systemctl restart nginx
Backup
It is important to make proper backups of your mail server. I chose to use Borgmatic for this. The following section should be regarded as pointers on how to do this. It is not a complete overview.
On both the mail server as well as the backup server, execute:
sudo apt install borgmatic
On the mail server, run:
sudo generate-borgmatic-config -d /etc/borgmatic/config.example.yaml
Look in /etc/borgmatic/config.example.yaml
for configuration examples.
sudo nano /etc/borgmatic/config.yaml
location:
# List of source directories to backup.
source_directories:
# Rspamd storage
- /var/lib/rspamd
- /etc/redis/redis.conf
- /var/lib/redis/dump.rdb
# Letsencrypt
- /etc/lego
# E-mail storage
- /var/vmail
# Web configuration pages such as autoconfig, mta-sts
- /var/www/
# Configuration files
- /etc/dovecot
- /etc/postfix
- /etc/rspamd
- /etc/nginx
# Paths of local or remote repositories to backup to.
repositories:
- backupuseraccountc@server.example.org:/var/borgbackup/repositories/mail
retention:
# Retention policy for how many backups to keep.
keep_hourly: 24
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
consistency:
# List of checks to run to validate your backups.
checks:
- repository
- archives
hooks:
# Databases to dump and include in backups.
postgresql_databases:
- name: vmail
hostname: 10.10.10.1
username: vmail
password: YOUR____PASSWORD
storage:
encryption_passphrase: "YOUR____PASSPHRASE"
compression: auto,lzma,9
Secure the config file by changing file ownership to root.
sudo chmod 600 /etc/borgmatic/config.yaml
Add the SSH key of the root user to the backup host.As root on the mail server, execute
borg init --encryption=keyfile-blake2 management@server.example.org:/var/borgbackup/repositories/mail
Make sure to export the borg backup key, which will be stored in /root/.config/borg/keys/
and will be named something like server_example_org_var_borgbackup_repositories_mail
. You could for example choose to store it on the backup server in the home folder of the backup user under .config/borg/keys
. In that case, remove the hostname from the key file. The aforementioned example key e.g. should be shortened to var_borgbackup_repositories_mail
. Make sure to also securely store the passphrase you used to unlock this key.
On the backup server, you can then check if the key has been setup properly by executing borg check /var/borgbackup/repositories/mail --info
if borg is installed.
sudo borgmatic --verbosity=1
Borgmatic, by default, automatically should run every 24 hours, as it configures a borgmatic.timer
in systemd.
Restore backup
It is advisable to try restoring these backups regularly, so you can be absolutely sure they work when you really need them.
Mail clients
- Thunderbird
- Install the Sieve plugin in order to be able to set up message filters in Thunderbird. Filters are set up on the server in Dovecot, therefore they will work across all devices.
Changelog
- 2024-04-10
- Add
mkdir .pyzor
command based on reader feedback.
- Add
- 2024-04-08
- Rspamd
- Add documentation for setting up Rspams’s phishing, razor and pyzor modules.
- Added DKIM reporting functionality.
- Add extended spam headers
- Lego
- Added Systemd timers for certificate renewal as an alternative to cron
- Borgmatic
- Removed superfluous borgmatic cron job, which was a leftover needed in previous Debian versions.
- Rspamd
- 2024-02-10
- Fixed changed DANE tool links
Sources
- Traefik documentation
- PostgreSQL data types
- Thomas Leister: Own mail server based on Dovecot, Postfix, MySQL, Rspamd and Debian 9 Stretch
- Thomas Leister: Mailserver mit Dovecot, Postfix, MySQL und Rspamd unter Debian 11 Bullseye / Ubuntu 22.04 LTS [v1.1]
- Rspamd packages
- Introducing SMTP TLS Reporting
- Internet.nl mail test
- MTA-STS Validator
- RFC8461 SMTP MTA Strict Transport Security (MTA-STS)
- Weakdh.org
- DANE SMTP Validator
- DANE implementation resources
- SSL Server Test
- RFC6698 The DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA
- New Adventures in DNSSEC and DANE
- Letsencrypt and DANE
- DANE und TLSA DNS-Records erklärt
- TLSA Record Generator
- Replacing antispam plugin with IMAPSieve
- ISPmail on Debian Buster – your mail server workshop
- borgmatic, borgmatic git
- Disable VRFY command