- Dovecot
- OpenDKIM
- Sender Policy Framework
- Postfix
- Amavis, SpamAssassin, and ClamAV
- fail2ban
- ufw
Over last few days, I have been gradually moving services from
an old server to a new one. This is a rather long-awaited upgrade.
To support such operations, I have bunch of documents describing the
current configuration and whatnot. Some documents are publicly available.
In 2019, description of my Postfix+Dovecot setup was still hosted here,
but at some point I decided to take it down.
That was a brilliant idea, because now that I read it... It's
terrible, my evening is ruined, and I had to do everything from scratch.
Join me today to configure an e-mail destination server, consisting
of Postfix acting as a Mail
Transfer Agent (MTA) and Dovecot
starring as a Mail Delivery Agent (MDA) and doing stuff Dovecot does
(aka IMAP server). This configuration will use system users, allows
for easy aliasing, Sieving, and hosting simple mailing lists.
Hopefully, this time it will be more useful. If you decide to use
this instruction, be wary, check listed sources and manual pages as you
go; review each encountered warning. If you encounter an error you can
or can't fix, send it to me and let's resolve it and document it here.
Dovecot
Packages: (Debian) dovecot-sieve, dovecot-core, dovecot-imapd
I start with Dovecot to untangle dependencies in Postfix right away.
Postfix can deliver into mailboxes just fine, but Dovecot is providing
Simple Authentication and Security Level (SASL), Local Delivery Agent
(LDA), Sieving, and IMAP. The first two are especially important.
Dovecot configuration is at /etc/dovecot/. Most distributions
will have conf.d/ subdirectory with configuration split into
files, but it can be completely ignored. I encourage you to review
it nonetheless.
Configuration can be checked with doveconf(1). Running it
without options will assume ‑n flag and show only the
settings with non-default values. Effective settings can be viewed with
‑a and defaults are ‑d. Some errors (e.g.,
syntax, typing) are checked when these commands are invoked.
We'll put everything into a single configuration file,
/etc/dovecot/dovecot.conf:
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
These are required by Dovecot.
mail_driver = Maildir
We'll only change the mail_driver, everything else around
is fine with the defaults. This will assume that user's mailboxes are
at ~user/Maildir.
ssl_server_cert_file = /path/to/secrets/fullchain.pem
ssl_server_key_file = /path/to/secrets/privkey.pem
Let's Encrypt certificates work just fine in this setup. We need
to kick Dovecot to reload them when refreshing.
auth_mechanisms = plain login
These two are plain text! This is not a problem, because we use SSL,
but if you plan to expose anything else than IMAPS, then consider whether
plain text passwords are OK for your method. Note that we will configure
pam as authentication driver which requires plain text.
protocols {
imap = yes
}
service imap-login {
inet_listener imaps {
ports = 993
ssl = yes
}
}
Enable IMAPS server on port 993 and SSL enabled using certificate-key
pair from above.
passdb pam {
driver = pam
}
userdb passwd {
driver = passwd
}
This setups is intended to use system users hence we set pam and
passwd drivers for overall user authentication.
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
group = postfix
user = postfix
}
}
Speaking of; we provide the SASL based on above drivers to postfix.
namespace inbox {
inbox = yes
mailbox "Drafts" {
auto = subscribe
special_use = \Drafts
}
mailbox "Sent" {
auto = subscribe
special_use = \Sent
}
mailbox "Trash" {
auto = subscribe
special_use = \Trash
}
mailbox "Junk" {
auto = subscribe
special_use = \Junk
}
mailbox "Archive" {
auto = subscribe
special_use = \Archive
}
}
This is a boilerplate for default inbox structure. Dovecot also has
a default, but I remember having some complaints about it, so this one
stuck with me for a long time. We'll use it for no other reason.
protocol lda {
mail_plugins {
sieve = yes
}
}
I use LDA, because it fits well with /etc/aliases used by
Postfix. I never got LMTP to work well with system users and aliases.
System users is doable if you tinker with username rewriting. Aliases
require even more effort, because you usually end up with a new passwd
database. In contrast, LDA works out of the box.
sieve_extensions {
editheader = yes
}
sieve_script personal {
sieve_script_path = ~/.sieve
}
sieve_script before {
sieve_script_path = /etc/dovecot/sieve/default-before.sieve
}
Above we enabled Sieve for LDA, now we configure Sieve itself.
editheader is there for my mailing lists scripts. Both paths
are arbitrary. personal is per-user and optional. I use
it for my mailing list.
That's it. Check config then restart Dovecot with systemctl
restart dovecot.
As for default-before.sieve it is similar to what you may find
in distributed examples:
require "fileinto";
if header :is "X-Spam-Flag" "YES"
{
fileinto "Junk";
}
Compile it:
# cd /etc/dovecot/sieve
# sievec default-before.sieve
OpenDKIM
Packages: (Debian) opendkim, opendkim-tools
DomainKeys Identified Mail (DKIM) is yet another identification
mechanisms to ensure good e-mail transits. OpenDKIM is an implementation
providing milter and key generation (among others).
Configuration is at /etc/opendkim.conf. A single domain
configuration is straight-forward; ensure following options are set:
Domain example.com
Selector sel
KeyFile /etc/dkimkeys/sel.private
Socket inet:8891@localhost
Selector must match /[a-z0-9][a-z0-9-]*/i; Debian uses
2020 in their documentation. sel in the key
file name is the selector name.
Generate appropriate key file:
# cd /etc/dkimkeys
# sudo -u opendkim opendkim-genkey -d example.com -s sel
Along with the key file, a handy sel.txt file will
be generated. This is a DNS entry. Put it in your DNS zone and once
it propagates you can test the key:
# opendkim-testkey -vvv
Remember the inet:8891@localhost; it is address of the
DKIM milter and will be used in Postfix.
Sender Policy Framework
This is entirely configured in the DNS zone. I use:
example.com. 600 IN TXT "v=spf1 a mx -all"
This translates to:
v=spf1
- This is an SPF version 1 entry.
a
- Sending is allowed if sender's IP matches the A or AAAA entry of the domain in question, or
mx
- If sender's IP matches the MX entry for the domain in question;
-all
- Everything else is forbidden.
OpenDMARC
Note: I should probably use it finally? But e-mails go through
without problems, do I really want to bother?
Postfix
Packages: (Debian) postfix
Finally, Postfix.
Here, we will also configure only few options and use defaults for
everything else. Configuration is at /etc/postfix/; there are two
files of interest: main.cf and master.cf. main.cf
is your regular configuration file that affects all of Postfix setup.
master.cf is a process configuration explained in detail by
master(5) and master(8). Postfix manual pages are good,
use them.
Let's start with main.cf:
compatibility_level = 3.9
This enforces specific defaults. Usually, the newer the safer.
myhostname = mail.example.com
I painfully learned to use subdomains for services. However, my
servers are not guaranteed to have the hostname applicable for the
service, so I state it explicitly here.
myorigin = $mydomain
$mydomain will be example.com in this
case. I only expect to send e-mails from the almost-top-level domain.
Note: This duplicates information in /etc/mailname from
Debian policy (and used by amavis).
mydestination = $mydomain, $myhostname, localhost.$mydomain, localhost
Still, I expect to receive some more domains. Local domains are
there mostly to avoid any kind of weird relaying loops. In this case,
the effective target domains would be: example.com,
mail.example.com, localhost.example.com,
and localhost.
mynetworks_style = host
relay_domains =
relayhost =
In short, we only act as a simple destination server and disable most
of attack vectors that could be used by someone to send spam through
our server.
message_size_limit = 20480000
For all the people loving to send huge docx or pdf files in
attachment...
smtpd_tls_security_level = encrypt
smtpd_tls_key_file = /path/to/secrets/privkey.pem
smtpd_tls_cert_file = /path/to/secrets/fullchain.pem
Same certificate-key pair as Dovecot.
smtpd_milters = inet:localhost:8891
non_smtpd_milters = $smtpd_milters
Use the milter provided by OpenDKIM we configured previously.
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = noanonymous
Use SASL service provided by Dovecot we configured previously.
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
One last bit of relay configuration. I put it together with SASL
configuration to remind me that permit_sasl_authenticated
option is used.
mailbox_command = /usr/lib/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT"
By default Postfix will use alias map in /etc/aliases, see
aliases(5) for details. mailbox_command has lower
precedence than aliases, so Dovecot's LDA will be used
after Postfix does alias resolution, see local(8) for details.
We use Dovecot here to trigger Sieve scripts configured previously.
Lastly, enable submission port in master.cf:
# service type private unpriv chroot wakeup maxproc command + args
smtp inet n - y - - smtpd
This is the default service you'll have somewhere at the top. No need
to do anything with it yet. Below, let's put secure submission service:
smtps inet n - y - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
smtp and smtps are defined in
/etc/services (simply grep it). See master(5) and
services(5) for details.
Check configuration with postconf(1); restart or reload the
Postfix service.
At this point, assuming ports are available from the network, the
server is up and running. The remaining configuration is firewall
and anti-spam.
Amavis, SpamAssassin, and ClamAV
Packages: (Debian) amavisd-new, clamav, clamav-daemon,
clamav-freshclam, clamdscan, spamassassin, libnet-dns-perl,
libnet-dns-perl, libmail-spf-perl, pyzor, razor
We will run anti-spam and anti-virus filtering through amavis. Postfix
will send received e-mails to Amavis, Amavis will use SpamAssassin and
ClamAV to perform the filtering, and then it will send back the e-mails
to special Postfix service that we will setup (to avoid looping the
e-mail in filtering hell).
Let's go back to /etc/postfix/master.cf to define the Postfix
part; somewhere at the bottom of master.cf add:
# service type private unpriv chroot wakeup maxproc command + args
amavisfeed unix - - n - 2 smtp
-o smtp_data_done_timeout=1200
-o smtp_send_xforward_command=yes
-o smtp_tls_note_starttls_offer=no
This will be used to forward e-mails for checks to Amavis from Postfix.
Now, find smtp and smtps entries at the top and add:
# service type private unpriv chroot wakeup maxproc command + args
smtp inet n - y - - smtpd
-o content_filter=amavisfeed:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings
smtps inet n - y - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o content_filter=amavisfeed:[127.0.0.1]:10024
-o receive_override_options=no_address_mappings
Amavis is by default listening on port 10024, hence
[127.0.0.1]:10024 is pointing at it. Now, Postfix will send
e-mails to Amavis, but we are still missing the return path. This one is
a bit more complex because we want to clear most options that are expected
of server that faces public network. At the bottom of master.cf:
# service type private unpriv chroot wakeup maxproc command + args
localhost:10025 inet n - n - 2 smtpd
-o content_filter=
-o smtpd_delay_reject=no
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o smtpd_data_restrictions=reject_unauth_pipelining
-o smtpd_end_of_data_restrictions=
-o smtpd_restriction_classes=
-o mynetworks=127.0.0.0/8
-o smtpd_error_sleep_time=0
-o smtpd_soft_error_limit=1001
-o smtpd_hard_error_limit=1000
-o smtpd_client_connection_count_limit=0
-o smtpd_client_connection_rate_limit=0
-o smtpd_tls_security_level=none
-o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters
-o local_header_rewrite_clients=
Long story short: allow incoming e-mails only from the current host
but trust it quite a bit (e.g., do not run milters).
To test whether this works, send an e-mail to a user in this server.
In the headers of received e-mail you should see Received:
hops related to Postfix-Amavis interaction (reverse order):
Received: from localhost (localhost [127.0.0.1])
by example.com (Postfix) with ESMTP id D463141209
for <user@example.com>; Mon, 10 Nov 2025 20:00:00 +0000 (UTC)
Received: from example.com ([127.0.0.1])
by localhost (example.com [127.0.0.1]) (amavis, port 10024)
with ESMTP id bFiYR05hnMRW for <user@example.com>;
Mon, 10 Nov 2025 20:00:00 +0000 (UTC)
OK, Postfix-Amavis works, but we don't have any filters enabled yet.
SpamAssassin will work as-is after we enabled it. ClamAV needs minimal
preparations, so let's take care of it first.
Amavis will forward content of e-mails to ClamAV via a file. ClamAV needs permissions to read those files:
# usermod -aG amavis clamav
clamav is the default user of ClamAV daemon and
amavis is group that the files "published" by Amavis
belong to.
Ensure that clamav-daemon and
clamav-freshclam services are enabled and running.
Now, enable filters in Amavis, read through
/etc/amavis/conf.d/15-content_filter_mode and follow instructions.
Expect to uncomment two maps, like so:
@bypass_virus_checks_maps = (
\%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
@bypass_spam_checks_maps = (
\%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
Send another test e-mail, check whether ClamAV scan results are
visible between the two Received: headers we noted earlier.
You can use a GTUBE string to test spam filtering, send a test e-mail
with following content:
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
Check syslog for any errors.
fail2ban
Packages: (Debian) fail2ban
Fail2ban configuration resides in /etc/fail2ban; I use
jail.local for local configuration. Let's ensure that Postfix
and Dovecot are watched by fail2ban:
[dovecot]
enabled = true
[postfix]
enabled = true
mode = auth
Restart the service and verify jails are enabled with
fail2ban-clien status.
ufw
Packages: (Debian) ufw
I use ufw to heavily limit any possible traffic. For this server
configuration, we need to add following rules:
allow smtp
allow smtps
allow imaps
Confirm with ufw status that ports 25, 465, and 993
are allowed. Make sure that OpenDKIM and Amavis ports are not accessible.