How to Setup an E-Mail Destination

  1. Dovecot
  2. OpenDKIM
  3. Sender Policy Framework
  4. Postfix
  5. Amavis, SpamAssassin, and ClamAV
  6. fail2ban
  7. 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.

dovecot? rooftops? dunno

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.

a fat rat

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.