Mail server (smtpd, dovecot, rspamd) on OpenBSD

Posted on September 17, 2020

Goal

  1. certificates with Let’s Encrypt (acme-client and the httpd server on OpenBSD)
  2. smtp and imap servers (opensmtpd and dovecot)
    • with virtual users for the domain example.com and local users
    • with STARTTLS for smtpd and TLS for imap
    • also, both virtual and local users have aliases
  3. antispam (rspamd + Sieve rules to train the antispam)

No fancy webmail.

This tutorial is heavily inspired by the vultr tutorial and an old article about let’s encrypt on openbsd.

Let’s Encrypt on OpenBSD

This is very straightforward.

  1. httpd has to be configured to point to the right directory for acme challenges for your domains
  2. acme-client has to know your domains
  3. DNS should be configured

That’s it.

Web Server

httpd.conf

# $OpenBSD: httpd.conf,v 1.20 2018/06/13 15:08:24 reyk Exp $

server "example.com" {
        alias "www.example.com"
        alias "mail.example.com"
        alias "team.example.com"
        listen on * port 80
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
        location "/special/*" {
                root "/htdocs/"
        }
        location * {
                block return 302 "https://$HTTP_HOST$REQUEST_URI"
        }
}

server "example.com" {
        alias "www.example.com"
        alias "mail.example.com"
        alias "team.example.com"
        listen on * tls port 443
        root "/htdocs/example.com"
        tls {
                certificate "/etc/ssl/example.com.fullchain.pem"
                key "/etc/ssl/private/example.com.key"
        }
        location "/pub/*" {
                directory auto index
        }
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
}

ACME client

acme-client.conf

#
# $OpenBSD: acme-client.conf,v 1.2 2019/06/07 08:08:30 florian Exp $
#
authority letsencrypt {
        api url "https://acme-v02.api.letsencrypt.org/directory"
        account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
        api url "https://acme-staging-v02.api.letsencrypt.org/directory"
        account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

domain example.com {
        alternative names { www.example.com mail.example.com team.example.com }
        domain key "/etc/ssl/private/example.com.key"
        domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
        sign with letsencrypt
}

DNS zone

In this example, I want example.com and three aliases. Here are the DNS records:

example.com       IN 1200 A 10.0.0.1
www.example.com   IN 1200 A 10.0.0.1
team.example.com  IN 1200 A 10.0.0.1

mail.example.com  IN 1200 A 10.0.0.1
example.com       IN 1200 MX 10 mail

You can also put CNAME instead of A records for team and www.

You can have free domain names on netlib.re!

Final steps

Lastly, we want to create acme and tls folders:

mkdir /var/www/acme
mkdir -p /etc/ssl/acme/private /etc/acme
chmod 0700 /etc/ssl/acme/private /etc/acme

Then we can restart httpd and launch acme-client:

rcctl restart httpd
acme-client example.com
# Second restart, now the web server has the right certificate.
rcctl restart httpd

Getting the software for the mail server

pkg_add opensmtpd-extras opensmtpd-filter-rspamd dovecot dovecot-pigeonhole rspamd redis

SMTP server

Virtual users

This tutorial uses an user vmail to handle all virtual users mails. vmail has the uid 2000, the gid 2000, and its home directory will be the root for virtual users maildirs: /var/vmail/.

touch /etc/mail/credentials
chmod 0440 /etc/mail/credentials
chown _smtpd:_dovecot /etc/mail/credentials
useradd -c "Virtual Users Mail Account" -d /var/vmail -s /sbin/nologin -u 2000 -g =uid -L staff vmail
mkdir /var/vmail
chown vmail:vmail /var/vmail

Credentials

In our setup, mail users will be either system users or virtual ones. The list of virtual users and their credentials need to be configured: this tutorial use a simple file to that end.

Generate the passwords

Each client needs to be authenticated before sending emails. Passwords need to be processed before being put in a simple text file.

smtpctl encrypt PASSWORD
# Example: smtpctl encrypt chocolate
# $2b$09$/aUucPECRPTasFpNZcgpEe.0TTSiJ9UXqhF4uIzFvlLr220nBxuMq

This way, passwords can be put in the credentials file.

The credential file

The /etc/mail/credentials file needs to be formatted, almost as /etc/passwd:

<email>:<password>:<user>:<uid>:<gid>:<maildir-path>::userdb_mail=maildir:<maildir-path>

Parameters user, uid and gid are related to the system user that will handle all virtual users mails: vmail. Also, maildir-path will be into the vmail home directory.

Here is an example for the user joe.satriani for the domain example.com.

joe.satriani@example.com:$2b$09$/aUucPECRPTasFpNZcgpEe.0TTSiJ9UXqhF4uIzFvlLr220nBxuMq:vmail:2000:2000:/var/vmail/example.com/joe.satriani::userdb_mail=maildir:/var/vmail/example.com/joe.satriani

Virtual users file

/etc/mail/virtuals defines the valid email addresses (and aliases) for our default domain example.com.

abuse@example.com: joe.satriani@example.com
hostmaster@example.com: joe.satriani@example.com
postmaster@example.com: joe.satriani@example.com
webmaster@example.com: joe.satriani@example.com

joe.satriani@example.com: vmail
steve.vai@example.com: vmail
john.petrucci@example.com: vmail

smtpd.conf

Before presenting the configuration file, here is a quick overview of the smtpd configuration parameters.

  • pki indicates certificate and key paths for a domain.
  • table references lists:
    • credentials for users
    • aliases to system users
    • virtual users, to deliver to non-system users
  • listen directive indicates:
    • IP addresses (or interfaces) and ports listened to for incoming emails
    • security: TLS and which pki to use
    • host name announced to clients and peers
  • filter executes an application on a mail
    • example: antispam
  • action describes what to do with a message
    • example: store the message in a mbox
    • example: store the message based on system users and aliases
    • example: store the message in a maildir using a virtual user table (non-system users)
  • match uses an action based on the processed message (origin, destination)
    • example: execute domain_mail action for emails for the domain example.com
    • example: execute local_mail action for emails coming from local address

Next step: the configuration file. I allow a machine in my domain (192.168.0.200) to send emails with this mail server as a relay.
The configuration file I use.

#       $OpenBSD: smtpd.conf,v 1.14 2019/11/26 20:14:38 gilles Exp $

# This is the smtpd server system-wide configuration file.
# See smtpd.conf(5) for more information.

pki "mail.example.com" cert "/etc/ssl/example.com.fullchain.pem"
pki "mail.example.com" key "/etc/ssl/private/example.com.key"

table aliases file:/etc/mail/aliases
table credentials passwd:/etc/mail/credentials
table virtuals file:/etc/mail/virtuals

# Filter potential spam with rspamd
filter "rspamd" proc-exec "/usr/local/libexec/smtpd/filter-rspamd"

# To accept external mail, replace with: listen on all
#
#listen on socket
listen on lo0
listen on "192.168.0.100" \
  tls pki "mail.example.com" \
  hostname "mail.example.com" filter "rspamd"

# Authorize people to send messages from our server
listen on "192.168.0.100" port submission \
  tls-require pki "mail.example.com" \
  hostname "mail.example.com" \
  auth <credentials> filter "rspamd"


# Where to store incoming emails based on the target user.
action "local_mail" mbox alias <aliases>
action "domain_mail" \
  maildir "/var/vmail/example.com/%{dest.user}" \
  virtual <virtuals>

# Relay mails when they come from authorized clients.
action "outbound" relay

# Next, we match incoming emails.
# When the mail comes from any place for our domain, it triggers the "domain_mail" action.
match from any for domain "example.com" action "domain_mail"
# When the mail comes from and for a local user it triggers the "local_mail" action.
match from local for local action "local_mail"

# HEADS UP: Authorize forwarding emails for a local machine
match from src "192.168.0.200" for any action "outbound"
# HEADS UP: Authorize forwarding emails for a local machine
match from local for any action "outbound"
match auth from any for any action "outbound"

The setup is simple: smtpd listens to local connections (on lo0) and on another interface (with the IP address 192.168.0.100). TLS is used along with the mail.example.com PKI for incoming connections (either for submissions or not). Incoming connections are filtered with rspamd and have mail.example.com as the provided host name. Submissions need to pass authentication based on the /etc/mail/credentials file set earlier.

This configuration allows any local or authenticated user to send mails to any domain. Besides the generic configuration in my setup, I authorized the forwarding of mails coming from a specific IP address in my local network (192.168.0.200).

Testing the mail server

# First, try the syntax.
doas smtpd -n
# Then run the service.
doas rcctl restart smtpd

Mails will be transfered in the /var/vmail/example.com/<username>/new directory.

Dovecot

Set the login class

Since dovecot opens a lot of files, it is preferable to create a login class for the service.

dovecot:\
  :openfiles-cur=1024:\
  :openfiles-max=2048:\
  :tc=daemon:
# Ensure new login class is taken into account.
cap_mkdb /etc/login.conf
# Change the dovecot login class.
usermod -L dovecot _dovecot

Dovecot configuration file

File /etc/dovecot/local.conf

auth_mechanisms = plain
first_valid_uid = 2000
first_valid_gid = 2000
mail_location = maildir:/var/vmail/%d/%n
mail_plugin_dir = /usr/local/lib/dovecot
managesieve_notify_capability = mailto
managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex  imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext imapsieve vnd.dovecot.imapsieve
mbox_write_locks = fcntl
mmap_disable = yes

namespace inbox {
  inbox = yes
  location =
  mailbox Archive {
  auto = subscribe
  special_use = \Archive
  }
  mailbox Drafts {
  auto = subscribe
  special_use = \Drafts
  }
  mailbox Junk {
  auto = subscribe
  special_use = \Junk
  }
  mailbox Sent {
  auto = subscribe
  special_use = \Sent
  }
  mailbox Trash {
  auto = subscribe
  special_use = \Trash
  }
  prefix =
}

passdb {
  args = scheme=CRYPT username_format=%u /etc/mail/credentials
  driver = passwd-file
  name =
}

plugin {
  imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_from = Junk
  imapsieve_mailbox2_name = *
  sieve = file:~/sieve;active=~/.dovecot.sieve
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
  sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
  sieve_plugins = sieve_imapsieve sieve_extprograms
}

protocols = imap sieve
service imap-login {
    inet_listener imaps {
    port = 993
  }
}

service managesieve-login {
  inet_listener sieve {
    port = 4190
  }
  inet_listener sieve_deprecated {
    port = 2000
  }
}

ssl_cert = </etc/ssl/example.com.fullchain.pem
ssl_key = </etc/ssl/private/example.com.key
userdb {
  args = username_format=%u /etc/mail/credentials
  driver = passwd-file
  name =
}

protocol imap {
  mail_plugins = " imap_sieve"
}

As of today (september 2020), there still is a bug in Dovecot: the ssl_cert and ssl_key settings do not get overridden in the local.conf file. We need to so we have to comment these parameters in the file /etc/dovecot/conf.d/10-ssl.conf.

Sieve scripts

To train rspamd, we need to provide sieve scripts. Training is done by moving emails into and out the junk folder. These files are located at /usr/local/lib/dovecot/sieve.

This archive contains the 4 files you need:

  • report-ham.sieve and report-spam.sieve
  • sa-learn-ham.sh and sa-learn-spam.sh

Once untar in /usr/local/lib/dovecot/sieve:

sievec report-ham.sieve
sievec report-spam.sieve
chmod 0755 sa-learn-ham.sh
chmod 0755 sa-learn-spam.sh

Then we can start the dovecot daemon.

rcctl enable dovecot
rcctl start dovecot

Finally, we can verify the setup by requesting informations about a client.

doveadm user joe.satriani@example.com

And verify that the user can log in.

doveadm auth login joe.satriani@example.com

rspamd

For rspamd, we need:

  • a pair of cryptographic keys
  • SPF, DKIM and DMARC DNS records
  • a redis server

First, the crytographic keys

doas su
mkdir /etc/mail/dkim
cd /etc/mail/dkim
openssl genrsa -out private.key 1024
openssl rsa -in private.key -pubout -out public.key
chmod 0440 private.key
chown root:_rspamd private.key

SPF

Of course, you have to put your own public IPv4 (or IPv6) address.

example.com. IN TXT "v=spf1 a ip4:192.168.0.100 mx ~all"

DKIM

default._domainkey.example.com. IN TXT "v=DKIM1;k=rsa;p=[…public key…]"

DMARC

_dmarc.example.com. IN TXT "v=DMARC1;p=none;pct=100;rua=mailto:postmaster@example.com"

Then we need to create the /etc/rspamd/local.d/dkim_signing.conf file.

domain {
    example.com {
        path = "/etc/mail/dkim/private.key";
        selector = "default";
    }
}

Final restart

Now, we have all the scripts, configuration files and services up and running except redis and rspamd.

doas rcctl enable redis rspamd
doas rcctl start redis rspamd