Build | GitHub | Mastodon | SourceHut |

Postfix is great software, no one can deny it. The only drawback of Postfix is its configuration. In the previous article we have setup Postfix with OpenDKIM and Dovecot.

I have read several articles on OpenSMTPD and interested to use it. In this article we describe the process to replace Postfix with OpenSMPTD.

We will use domain name "" as an example in the script or configuration later. Make sure to replace them accordingly, if you copy-paste part of the script or commands from this article.

In this article, we use awwan script for configuration management, so if you see string like these {{.Val "section:sub:key"}} that means its an awwan variable and needs to be replaced according to your environment.

Email architecture with OpenSMTPD

The following diagram show the email architecture using OpenSMTPD, Rspamd, and Dovecot.

             Client <------------------------------+
               |                                   |
               | SMTPS                             | IMAP
               | :465/:587                         | :993
      SMTP     v                                   |
      :25  +-----------+                         +---------+
MDA <----->| OpenSMTPD |------------------------>| Dovecot |
           +-----------+  /var/run/dovecot/lmtp  +---------+
           | Rspamd |

We use Rspamd to sign outgoing messages using DKIM and to check spam for incoming messages, replacing OpenDKIM.

Setting up Dovecot

The following awwan script show how to setup Dovecot in Arch Linux,

sudo pacman -Syu --noconfirm
sudo pacman -S --noconfirm dovecot

sudo systemctl enable dovecot

sudo groupadd {{.Val "email::group"}} -g {{.Val "email::gid"}}

sudo useradd  {{.Val "email::user"}} \
	-r \
	-g {{.Val "email::gid"}} \
	-u {{.Val "email::uid"}} \
	-d {{.Val "email::dir"}} \
	-m -c "mail user"

sudo mkdir -p /etc/dovecot

## Generate password for IMAP.

#put: {{.ScriptDir}}/etc/dovecot/passwd.txt passwd.txt

rm -f passwd

while read -r email plain; do \
	hash=$(doveadm pw -s SHA1 -p "$plain"); \
	echo "$email:$hash:::" >> passwd; \
done < passwd.txt

cat passwd

#get: passwd {{.ScriptDir}}/etc/dovecot/passwd

sudo mv passwd /etc/dovecot/passwd
rm -f passwd.txt

#put! {{.ScriptDir}}/etc/dovecot/dovecot.conf /etc/dovecot/

sudo chmod 0600 /etc/dovecot/{dovecot.conf,passwd}
sudo chown -R dovecot:dovecot /etc/dovecot

sudo systemctl restart dovecot
sudo systemctl status  dovecot

There is not much changes with previous setup, only inside the dovecot.conf.

Example of passwd.txt content and format (not the actual password),          s333cr333t

Example of passwd output (not the actual hash),


listen =
protocols = lmtp imap
disable_plaintext_auth = yes
auth_mechanisms = plain login
mail_access_groups = {{.Val "email::group"}}
default_login_user = {{.Val "email::user"}}
first_valid_uid = {{.Val "email::uid"}}
first_valid_gid = {{.Val "email::gid"}}
mail_location = maildir:~/

passdb {
	driver = passwd-file
	args = scheme=SHA1 /etc/dovecot/passwd

userdb {
	driver = passwd-file
	args = /etc/dovecot/passwd
	default_fields = uid=vmail gid=vmail home=/data/vmail/%d/%n

service lmtp {
	unix_listener lmtp {
		user  = {{.Val "email::user"}}
		group = {{.Val "email::group"}}

service imap-login {
	process_min_avail = 3
	user = {{.Val "email::user"}}

	inet_listener imap {
	inet_listener imaps {
		port = 993
		ssl = yes

namespace inbox {
	inbox = yes

	mailbox Trash {
		auto = no
		special_use = \Trash
	mailbox Drafts {
		auto = no
		special_use = \Drafts
	mailbox Sent {
		auto = subscribe # autocreate and autosubscribe the Sent mailbox
		special_use = \Sent
	mailbox Spam {
		auto = create # autocreate Spam, but don't autosubscribe
		special_use = \Junk

##--- SSL/TLS

ssl = required
ssl_cipher_list = HIGH:!SSLv2:!aNULL@STRENGTH
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes

ssl_cert = </etc/letsencrypt/live/
ssl_key = </etc/letsencrypt/live/
ssl_dh = </etc/haproxy/dhparam

local_name {
  ssl_cert = </etc/letsencrypt/live/
  ssl_key  = </etc/letsencrypt/live/

One of the changes is in the "userdb". Previously we use "static" now we make it to read the list of user from the same file as "passdb".

We add service "lmtp" and remove "imap-login". Service "lmtp" will create UNIX socket under "/var/run/dovecot/lmtp" under user and group "vmail". This socket will be used by OpenSMTPD for email delivery.

Setting up Rspamd

Rspamd is a fast, free and open-source spam filtering system. It can be also used for DKIM signing and validation.

Since we already have public and private key from previous article, we can just use it and move it to rspamd location.

If you have not have the keys, we can create it using the following commands as reference in the local host,

openssl genrsa -out 1024
openssl rsa -in \

mkdir {{.ScriptDir}}/var/lib/rpamd/dkim/

mv \
mv \

The content of "" is public key that can be used to set DNS TXT record for DKIM.

The following awwan script setup rspamd,

 1: sudo pacman -Sy --noconfirm opensmtpd-filter-rspamd

 2: sudo systemctl enable rspamd.service

 3: sudo mkdir -p /var/lib/rspamd/dkim/

 4: #put! {{.ScriptDir}}/var/lib/rspamd/dkim/ \

 5: sudo chown -R rspamd:rspamd /var/lib/rspamd/dkim
 6: sudo chmod 0600 /var/lib/rspamd/dkim/*

 7: sudo mkdir -p /etc/rspamd/local.d/

 8: #put! {{.ScriptDir}}/etc/rspamd/local.d/dkim_signing.conf \

 9: sudo chown -R rspamd:rspamd /etc/rspamd/local.d/

10: sudo systemctl restart rspamd.service

11: sudo journalctl -u rspamd.service -f

In line 1, we install opensmtpd-filter-rspamd (which implicitly install rspamd) for integrating with opensmtpd. opensmtpd-filter-rspamd provide a binary that will be executed by OpenSMTPD for each incoming messages.

The only configuration we need to add is dkim_signing.conf:

allow_username_mismatch = true;

domain { {
		path = "/var/lib/rspamd/dkim/";
		selector = "20210411-1";

The rest of rspamd can be leave it as is.

Setting up OpenSMTPD

The installation process for OpenSMTPD is straight forward, except that we need to uninstall postfix first since both of them are in conflict in Arch Linux.

The following awwan script show how to do it,

## Uninstall postfix first, since its conflict with opensmtpd.

sudo systemctl stop postfix.service
sudo systemctl disable postfix.service
sudo pacman -Rs --noconfirm postfix

## Uninstall opendkim.

sudo systemctl stop opendkim.service
sudo systemctl disable opendkim.service

sudo pacman -Rs --noconfirm opendkim

## Install smtpd.

sudo pacman -Sy --noconfirm opensmtpd

sudo systemctl enable smtpd.service

## Generate passwords.

#put: {{.ScriptDir}}/etc/smtpd/passwds.txt passwds.txt

rm -f passwds

while read -r email plain; do \
	hash=$(smtpctl encrypt "$plain"); \
	echo "$email $hash" >> passwds; \
done < passwds.txt

cat passwds

sudo mv passwds /etc/smtpd/passwds

## Setup...

#put! {{.ScriptDir}}/etc/smtpd/aliases         /etc/smtpd/aliases
#put! {{.ScriptDir}}/etc/smtpd/virtual_aliases /etc/smtpd/virtual_aliases
#put! {{.ScriptDir}}/etc/smtpd/virtual_domains /etc/smtpd/virtual_domains
#put! {{.ScriptDir}}/etc/smtpd/virtual_users   /etc/smtpd/virtual_users
#put! {{.ScriptDir}}/etc/smtpd/smtpd.conf      /etc/smtpd/smtpd.conf

sudo chown -R smtpd:smtpd /etc/smtpd/
sudo chmod 0600 /etc/smtpd/*

sudo smtpd -n -v

sudo systemctl restart smtpd.service

The passwds.txt is plain text file that contains user and password for submission using SMTP (through port 465). My recommendation is to use the same password between Dovecot (IMAP) and SMTP.

Example of passwds.txt (not the actual password), s3333cr3333t

Example of generated passwds (not the actual hash), $y$jCT$X0QZ...


#	$OpenBSD: aliases,v 1.67 2019/01/26 10:58:05 florian Exp $
#  Aliases in this file will NOT be expanded in the header from
#  Mail, but WILL be visible over networks or from /usr/libexec/mail.local.
#	>>>>>>>>>>	The program "newaliases" must be run after
#	>> NOTE >>	this file is updated for any changes to
#	>>>>>>>>>>	show through to smtpd.

# Basic system aliases -- these MUST be present
MAILER-DAEMON: postmaster
postmaster: root

# General redirections for important pseudo accounts
daemon:	root
ftp-bugs: root
operator: root
www:	root

# Redirections for pseudo accounts that should not receive mail
_bgpd: /dev/null
_dhcp: /dev/null
_dpb: /dev/null
_dvmrpd: /dev/null
_eigrpd: /dev/null
_file: /dev/null
_fingerd: /dev/null
_ftp: /dev/null
_hostapd: /dev/null
_identd: /dev/null
_iked: /dev/null
_isakmpd: /dev/null
_iscsid: /dev/null
_ldapd: /dev/null
_ldpd: /dev/null
_mopd: /dev/null
_nsd: /dev/null
_ntp: /dev/null
_ospfd: /dev/null
_ospf6d: /dev/null
_pbuild: /dev/null
_pfetch: /dev/null
_pflogd: /dev/null
_ping: /dev/null
_pkgfetch: /dev/null
_pkguntar: /dev/null
_portmap: /dev/null
_ppp: /dev/null
_rad: /dev/null
_radiusd: /dev/null
_rbootd: /dev/null
_relayd: /dev/null
_rebound: /dev/null
_ripd: /dev/null
_rstatd: /dev/null
_rusersd: /dev/null
_rwalld: /dev/null
_smtpd: /dev/null
_smtpq: /dev/null
_sndio: /dev/null
_snmpd: /dev/null
_spamd: /dev/null
_switchd: /dev/null
_syslogd: /dev/null
_tcpdump: /dev/null
_traceroute: /dev/null
_tftpd: /dev/null
_unbound: /dev/null
_unwind: /dev/null
_vmd: /dev/null
_x11:   /dev/null
_ypldap: /dev/null
bin:	/dev/null
build:	/dev/null
nobody:	/dev/null
_tftp_proxy: /dev/null
_ftp_proxy: /dev/null
_sndiop: /dev/null
_syspatch: /dev/null
_slaacd: /dev/null
sshd:   /dev/null

# Well-known aliases -- these should be filled in!
root: ms
# manager:
# dumper:

abuse:		root
# noc:		root
security:	root

# hostmaster:	root
# usenet:	root
# news:		usenet
# webmaster:	root
# ftp:		root

This is an alias that forward email for system user account. In general, all email will be handled by user "ms" and then forwarded to "".

/etc/smtpd/virtual_aliases:         vmail

In the virtual_aliases we forward all incoming email for "" to user "vmail" (which connect opensmtpd with dovecot later).


In the virtual_domains we list all domains that we want to handle by this email server.


In the virtual_users we list all virtual email addresses for better spam protection later.


## References,
## 1)
## 2)

pki "" cert "/etc/letsencrypt/live/"
pki "" key  "/etc/letsencrypt/live/"

table aliases           "/etc/smtpd/aliases"
table passwds           "/etc/smtpd/passwds"
table virtual_aliases   "/etc/smtpd/virtual_aliases"
table virtual_domains   "/etc/smtpd/virtual_domains"
table virtual_users     "/etc/smtpd/virtual_users"

## Generated by "head -c 30 /dev/urandom | base64"
## Replace once a year, move key to backup, and then generate new key.
srs key "MduE9i1NoI1zvtitetjcnktlcuPY4xjjjllSNKmY"
srs key backup "JCra7rL7z+69yzgLJ0MTOeOiOxFcelrTpqxJhQVV"

filter   "rdns" phase connect match   !rdns disconnect "550 DNS error"
filter "fcrdns" phase connect match !fcrdns disconnect "550 DNS error"
## From package: opensmtpd-filter-rspamd
filter "rspamd" proc-exec "/usr/lib/smtpd/opensmtpd/filter-rspamd"

## Inbound.

listen on eth0 port 25 tls pki "" filter { "rdns", "fcrdns", "rspamd" }

action "remote" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual <virtual_aliases>
action "local" lmtp "/var/run/dovecot/lmtp" rcpt-to alias <aliases>
#match from any for domain <virtual_domains> action "remote"
match from any for rcpt-to <virtual_users> action "remote"
match from local for local action "local"

## Outbound.

listen on eth0 port 465 smtps       pki "" auth <passwds> filter "rspamd"
listen on eth0 port 587 tls-require pki "" auth <passwds> filter "rspamd"
action "SEND" relay srs
match from any auth for any action "SEND"

For incoming email from other MDA, we open port 25 and filter it using rdns, fcrdns, and rspamd. We define two actions, one for "remote" (external) and one for "local" (between users in system). For the "remote" if the "RCPT TO" command match with one of item in "virtual_users" forward to lmtp at "/var/run/dovecot/lmtp", which is handled by Dovecot. Dovecot will take over from this point.

For outgoing email from port 465 or 587, client need to be authenticated using username and password that we define in file "passwds". If the authentication is valid, we relay it. The srs (Sender Rewriting Scheme) is optional, it could be useful if you define an alias that forward an email from different domain in your /etc/smptd/aliases, for example ""

Setting up fail2ban

The current fail2ban does not have opensmtpd rules, so we need to create it manually.

The following script show how to setup fail2ban (assuming its already installed),

 1: #put! {{.ScriptDir}}/etc/fail2ban/filter.d/opensmtpd.local \

## Test

 2: fail2ban-regex systemd-journal /etc/fail2ban/filter.d/opensmtpd.local

 3: #put! {{.ScriptDir}}/etc/fail2ban/jail.local     /etc/fail2ban/jail.local

 4: sudo systemctl restart fail2ban
 5: sudo systemctl status  fail2ban

In line 1, we create new local filter, see the content below.

In line 2, we test the filter by running fail2ban-regex command against systemd-journal.

In line 3, we modified the list of filter to be enabled, remove the postfix-sasl and enable our opensmptd filter.


## Fail2Ban filter for opensmtpd
## Author: Shulhan <ms at kilabit dot info>

prefregex = <F-MLFID>: \w{16} </F-MLFID><F-CONTENT>.+</F-CONTENT>$

failregex = <F-NOFAIL>smtp connected address=(?:<IP6>|<IP4>)</F-NOFAIL>
            smtp failed-command command="" result="550 DNS error"
            smtp failed-command command="AUTH LOGIN" result="503 5.5.1 Invalid command: Command not supported"
            <F-NOFAIL><F-MLFFORGET>smtp disconnected</F-MLFFORGET></F-NOFAIL>

mode = normal
ignoreregex =
journalmatch = _SYSTEMD_UNIT=smtpd.service

The rule said like these: read journal log where _SYSTEMD_UNIT is "smtpd.service". If the first line start with "smtp connected address=" capture its IP6 or IP4 address. If the next line match with

smtp failed-command command="" result="550 DNS error"

(This is the error throw by opensmtpd filter that we setup earlier)

filter   "rdns" phase connect match   !rdns disconnect "550 DNS error"

its means someone trying to submit through port 25 but the PTR record does not match, then jail it! Or, if the next line match with

smtp failed-command command="AUTH LOGIN" result="503 5.5.1 Invalid
command: Command not supported"

which means someone try submit through SMTPS with invalid authentication, then jail it too!


ignoreip = ::1
bantime  = 1w
banaction = nftables
banaction_allports = nftables[type=allports]

enabled = true
backend = systemd

enabled  = true
bantime  = -1
maxretry = 1
backend  = systemd

In the "jail.local" we remove the "postfix-sasl" rule and add our new "opensmtpd".

That’s it, happy emailing!