Title: Full-featured email server running OpenBSD | |
Author: Solène | |
Date: 24 July 2024 | |
Tags: nocloud selfhosting openbsd security | |
Description: In this blog post, you will learn everything you know to | |
setup a secure and low maintenance email server | |
# Introduction | |
This blog post is a guide explaining how to setup a full-featured email | |
server on OpenBSD 7.5. It was commissioned by a customer of my | |
consultancy who wanted it to be published on my blog. | |
Setting up a modern email stack that does not appear as a spam platform | |
to the world can be a daunting task, the guide will cover what you need | |
for a secure, functional and low maintenance email system. | |
The features list can be found below: | |
* email access through IMAP, POP or Webmail | |
* secure SMTP server (mandatory server to server encryption, personal | |
information hiding) | |
* state-of-the-art setup to be considered as legitimate as possible | |
* firewall filtering (bot blocking, all ports closes but the required | |
ones) | |
* anti-spam | |
In the example, I will set up a temporary server for the domain | |
`puffy.cafe` with a server using the subdomain `mail.puffy.cafe`. From | |
there, you can adapt with your own domain. | |
# Quick reminder | |
I prepared a few diagrams explaining how all the components are used | |
together, in three cases: when sending an email, when the SMTP servers | |
receives an email from the outside and when you retrieve your emails | |
locally. | |
Authenticated user sending an email to the outside | |
Outside sending an email to one of our users | |
User retrieving emails for reading | |
# Packet Filter (PF) | |
Packet Filter is OpenBSD's firewall. In our setup, we want all ports | |
to be blocked except the few ones required for the email stack. | |
The following ports will be required: | |
* opensmtpd 25/tcp (smtp): used for email delivery from other servers, | |
supports STARTTLS | |
* opensmtpd 465/tcp (smtps): used to establish a TLS connection to the | |
SMTP server to receive or send emails | |
* opensmtpd 587/tcp (submission): used to send emails to external | |
servers, supports STARTTLS | |
* httpd 80/tcp (http): used to generate TLS certificates using ACME | |
* dovecot 993/tcp (imaps): used to connect to the IMAPS server to read | |
emails | |
* dovecot 995/tcp (pop3s): used to connect to the POP3S server to | |
download emails | |
* dovecot 4190/tcp (sieve): used to allow remote management of an user | |
SIEVE rules | |
Depending on what services you will use, only the opensmtpd ports are | |
mandatory. In addition, we will open the port 22/tcp for SSH. | |
```pf.conf | |
set block-policy drop | |
set loginterface egress | |
set skip on lo0 | |
# normalisation des paquets | |
match in all scrub (no-df random-id max-mss 1440) | |
antispoof quick for { egress } | |
tcp_ports = "{ smtps smtp submission imaps pop3s sieve ssh http }" | |
block all | |
pass out inet | |
pass out inet6 | |
# allow ICMP (ping) | |
pass in proto icmp | |
# allow IPv6 to work | |
pass in on egress inet6 proto icmp6 all icmp6-type { routeradv neighbrsol neigh… | |
pass in on egress inet6 proto udp from fe80::/10 port dhcpv6-server to fe80::/1… | |
# allow our services | |
pass in on egress proto tcp from any to any port $tcp_ports | |
# default OpenBSD rules | |
# By default, do not permit remote connections to X11 | |
block return in on ! lo0 proto tcp to port 6000:6010 | |
# Port build user does not need network | |
block return out log proto {tcp udp} user _pbuild | |
``` | |
# DNS | |
If you want to run your own email server, you need a domain name | |
configured with a couple of DNS records about the email server. | |
## MX records | |
Wikipedia page: MX record | |
The MX records list the servers that should be used by outside SMTP | |
servers to send us emails, this is the public list of our servers | |
accepting emails for a given domain. They have a weight associated to | |
each of them, the server with the lowest weight should be used first | |
and if it does not respond, the next server used will be the one with a | |
slightly higher weight. This is a simple mechanism that allow setting | |
up a hierarchy. | |
I highly recommend setting up at least two servers, so if your main | |
server fails is unreachable (host outage, hardware failure, upgrade | |
ongoing) the emails will be sent to the backup server. Dovecot bundles | |
a program to synchronize mailboxes between servers, one way or two-way, | |
one shot or continuously. | |
If you have no MX records in your domain name, it is not possible to | |
send you emails. It is like asking someone to send you a post card | |
without giving them any clue about your real address. | |
Your server hostname can be different from the domain apex (raw domain | |
name without a subdomain), a simple example would be to use | |
`mail.domain.example` for the server name, this will not prevent it | |
from receiving/sending emails using `@domain.example` in email | |
addresses. | |
In my example, the domain puffy.cafe mail server will be | |
mail.puffy.cafe, giving this MX record in my DNS zone: | |
```dns | |
IN MX 10 mail.puffy.cafe. | |
``` | |
## SPF | |
Wikipedia page: SPF record | |
The SPF record is certainly the most important piece of the email | |
puzzle to detect spam. With the SPF, the domain name owner can define | |
which servers are allowed to send emails from that domain. A properly | |
configured spam filter will give a high spam score to incoming emails | |
that are not in the sender domain SPF. | |
To ease the configuration, that record can automatically include all MX | |
defined for a domain, but also A/AAAA records, so if you only use your | |
MX servers for sending, a simple configuration allowing MX servers to | |
send is enough. | |
In my example, only mail.puffy.cafe should be legitimate for sending | |
emails, any future MX server should also be allowed to send emails, so | |
we configure the SPF to allow all MX defined servers to be senders. | |
```dns | |
600 IN TXT "v=spf1 mx -all" | |
``` | |
## DKIM | |
Wikipedia page: DKIM signature | |
When used, the DKIM is a system allowing a receiver to authenticate a | |
sender, based on an asymmetric cryptographic keys. The sender | |
publishes its public key on a TXT DNS record before signing all | |
outgoing emails using the private key. By doing so, receivers can | |
validate the email integrity and make sure it was sent from a server of | |
the domain claimed in the From header. | |
DKIM is mandatory to not be classified as a spamming server. | |
The following set of commands will create a 2048 bits RSA key in | |
`/etc/mail/dkim/private/puffy.cafe.key` with its public key in | |
`/etc/mail/dkim/puffy.cafe.pub`, the `umask 077` command will make sure | |
any file created during the process will only be readable by root. | |
Finally, you need to make the private key readable to the group | |
`_rspamd`. | |
You need to install the rspamd package to have the user `_rspamd` for | |
these instructions. If you want to use a different DKIM program, you | |
will need to adapt the configuration. | |
``` | |
pkg_add rspamd-- opensmtpd-filter-rspamd | |
``` | |
Note: the umask command will persist in your shell session, if you do | |
not want to create files/directory only readable by root after this, | |
either spawn a new shell, or run the set of commands in a new shell and | |
then exit from it once you are done. | |
``` | |
umask 077 | |
install -d -o root -g wheel -m 755 /etc/mail/dkim | |
install -d -o root -g _rspamd -m 775 /etc/mail/dkim/private | |
openssl genrsa -out /etc/mail/dkim/private/puffy.cafe.key 2048 | |
openssl rsa -in /etc/mail/dkim/private/puffy.cafe.key -pubout -out /etc/mail/dk… | |
chgrp _rspamd /etc/mail/dkim/private/puffy.cafe.key /etc/mail/dkim/private/ | |
chmod 440 /etc/mail/dkim/private/puffy.cafe.key | |
chmod 775 /etc/mail/dkim/private/ | |
``` | |
In this example, we will name the DKIM selector `dkim` to keep it | |
simple. The selector is the name of the key, this allows having | |
multiple DKIM keys for a single domain. | |
Add the DNS record like the following, the value in `p` is the public | |
key in the file `/etc/mail/dkim/puffy.cafe.pub`, you can get it as a | |
single line with the command `awk '/PUBLIC/ { $0="" } { printf | |
("%s",$0) } END { print }' /etc/mail/dkim/puffy.cafe.pub`: | |
Your registrar may offer to add the entry using a DKIM specific form. | |
There is nothing wrong doing so, just make sure the produced entry | |
looks like the entry below. | |
``` | |
dkim._domainkey IN TXT "v=DKIM1;k=rsa;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgK… | |
``` | |
## DMARC | |
Wikipedia page: DMARC record | |
The DMARC record is an extra mechanism that comes on top of SPF/DKIM, | |
while it does not do much by itself, it is important to configure it. | |
DMARC could be seen as a public notice explaining to servers receiving | |
emails whose sender looks like your domain name (legit or not) what | |
they should do if SPF/DKIM does not validate. | |
As of 2024, DMARC offers three actions for receivers: | |
* do nothing but make a report to the domain owner | |
* "quarantine" mode: tell the receiver to be suspicious without | |
rejecting it, the result will depend on the receiver (most of the time | |
it will be flagged as spam) and make a report | |
* "reject" mode: tell the receiver to not accept the email and make a | |
report | |
In my example, I want invalid SPF/DKIM emails to be rejected. It is | |
quite arbitrary, but I prefer all invalid emails from my domain to be | |
discarded rather than ending up in a spam directory, so `p` and `sp` | |
are set to `reject`. In addition, if my own server is misconfigured I | |
will be notified about delivery issues sooner than if emails were | |
silently put into quarantine. | |
An email address should be provided to receive DMARC reports, they are | |
barely readable and I never made use of them, but the email address | |
should exist so this is what the `rua` field is for. | |
The field `aspf` is set to `r` (relax), basically this allows any | |
servers with a hostname being a subdomain of `.puffy.cafe` to send | |
emails for `@puffy.cafe`, while if this field is set to `s` (strict), | |
the domain of the sender should match the domain of the email server | |
(`mail.puffy.cafe` would only be allowed to send for | |
`@mail.puffy.cafe`). | |
Mx Toolbox website: DMARC tags list | |
``` | |
_dmarc IN TXT "v=DMARC1;p=reject;rua=mailto:[email protected];sp=reje… | |
``` | |
## PTR (Reverse DNS) | |
Wikipedia page: PTR record | |
An older mechanism used to prevent spam was to block, or consider as | |
spam, any SMTP server whose advertised hostname did not match the | |
result of the reverse lookup of its IP. | |
Let's say "mail.foobar.example" (IP: A.B.C.D) is sending an email to my | |
server, if the result of the DNS request to resolve the PTR of A.B.C.D | |
is not "mail.foobar.example", the email would be considered as spam or | |
rejected. While this is superseded by SPF/DKIM and annoying as it is | |
not always possible to define a PTR for a public IP, the reverse DNS | |
setup is still a strong requirement to not be considered as a spamming | |
platform. | |
Make sure the PTR matches the system hostname and not the domain name | |
itself, in the example above the PTR should be `mail.foobar.example` | |
and not `foobar.example`. | |
# System configuration | |
## Acme-client | |
The first step is to obtain a valid TLS certificate, this requires | |
configuring acme-client, httpd and start httpd daemon. | |
Copy the acme-client example `cp /etc/examples/acme-client.conf /etc/` | |
Modify `/etc/acme-client.conf` and edit only the last entry to | |
configure your own domain, mine looks like this: | |
``` | |
# | |
# $OpenBSD: acme-client.conf,v 1.5 2023/05/10 07:34:57 tb 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" | |
} | |
authority buypass { | |
api url "https://api.buypass.com/acme/directory" | |
account key "/etc/acme/buypass-privkey.pem" | |
contact "mailto:[email protected]" | |
} | |
authority buypass-test { | |
api url "https://api.test4.buypass.no/acme/directory" | |
account key "/etc/acme/buypass-test-privkey.pem" | |
contact "mailto:[email protected]" | |
} | |
domain mail.puffy.cafe { | |
# you can remove the line "alternative names" if you do not need extra subd… | |
# associated to this certificate | |
# imap.puffy.cafe is purely an example, I do not need it | |
alternative names { imap.puffy.cafe pop.puffy.cafe } | |
domain key "/etc/ssl/private/mail.puffy.cafe.key" | |
domain full chain certificate "/etc/ssl/mail.puffy.cafe.fullchain.pem" | |
sign with letsencrypt | |
} | |
``` | |
Now, configure httpd, starting from the OpenBSD example: `cp | |
/etc/examples/httpd.conf /etc/` | |
Edit `/etc/httpd.conf`, we want the first block to match all domains | |
but not "example.com", and we do not need the second block listen on | |
443/tcp (except if you want to run a https server with some content, | |
but you are on your own then). The resulting file should look like the | |
following: | |
```httpd.conf | |
# $OpenBSD: httpd.conf,v 1.22 2020/11/04 10:34:18 denis Exp $ | |
server "*" { | |
listen on * port 80 | |
location "/.well-known/acme-challenge/*" { | |
root "/acme" | |
request strip 2 | |
} | |
location * { | |
block return 302 "https://$HTTP_HOST$REQUEST_URI" | |
} | |
} | |
``` | |
Enable and start httpd with `rcctl enable httpd && rcctl start httpd`. | |
Run `acme-client -v mail.puffy.cafe` to generate the certificate with | |
some verbose output (if something goes wrong, you will have a clue). | |
If everything went fine, you should have the full chain certificate in | |
`/etc/ssl/mail.puffy.cafe.fullchain.pem` and the private key in | |
`/etc/ssl/private/mail.puffy.cafe.key`. | |
## Rspamd | |
You will use rspamd to filter spam and sign outgoing emails for DKIM. | |
Install rspamd and the filter to plug it to opensmtpd: | |
```shell | |
pkg_add rspamd-- opensmtpd-filter-rspamd | |
``` | |
You need to configure rspamd to sign outgoing emails with your DKIM | |
private key, to proceed, create the file | |
`/etc/rspamd/local.d/dkim_signing.conf` (the filename is important): | |
``` | |
# our usernames does not contain the domain part | |
# so we need to enable this option | |
allow_username_mismatch = true; | |
# this configures the domain puffy.cafe to use the selector "dkim" | |
# and where to find the private key | |
domain { | |
puffy.cafe { | |
path = "/etc/mail/dkim/private/puffy.cafe.key"; | |
selector = "dkim"; | |
} | |
} | |
``` | |
For better performance, you need to use redis as a cache backend for | |
rspamd: | |
```shell | |
rcctl enable redis | |
rcctl start redis | |
``` | |
Now you can start rspamd: | |
``` | |
rcctl enable rspamd | |
rcctl start rspamd | |
``` | |
For extra information about rspamd (like statistics or its web UI), I | |
wrote about it in 2021: | |
Older blog post: 2024-07-13 Filtering spam using Rspamd and OpenSMTPD on OpenBSD | |
### Alternatives | |
If you do not want to use rspamd, it is possible to replace the DKIM | |
signing part using `opendkim`, `dkimproxy` or | |
`opensmtpd-filter-dkimsign`. The spam filter could be either replaced | |
by the featureful `spamassassin` available as a package, or partially | |
with the base system program `spamd` (it does not analyze emails). | |
This guide only focus on rspamd, but it is important to know | |
alternatives exist. | |
## OpenSMTPD | |
OpenSMTPD configuration file on OpenBSD is `/etc/mail/smtpd.conf`, here | |
is a working configuration with a lot of comments: | |
```smtpd.conf | |
## this defines the paths for the X509 certificate | |
pki puffy.cafe cert "/etc/ssl/mail.puffy.cafe.fullchain.pem" | |
pki puffy.cafe key "/etc/ssl/private/mail.puffy.cafe.key" | |
pki puffy.cafe dhe auto | |
## this defines how the local part of email addresses can be split | |
# defaults to '+', so solene+foobar@domain matches user | |
# solene@domain. Due to the '+' character being a regular source of issues | |
# with many online forms, I recommend using a character such as '_', | |
# '.' or '-'. This feature is very handy to generate infinite unique emails | |
# addresses without pre-defining aliases. | |
# Using '_', solene_openbsd@domain and solene_buystuff@domain lead to the | |
# same address | |
smtp sub-addr-delim '_' | |
## this defines an external filter | |
# rspamd does dkim signing and spam filter | |
filter rspamd proc-exec "filter-rspamd" | |
## this defines which file will contain aliases | |
# this can be used to define groups or redirect emails to users | |
table aliases file:/etc/mail/aliases | |
## this defines all the ports to use | |
# mask-src hides system hostname, username and public IP when sending an email | |
listen on all port 25 tls pki "puffy.cafe" filter "rspamd" | |
listen on all port 465 smtps pki "puffy.cafe" auth mask-src filter "rspam… | |
listen on all port 587 tls-require pki "puffy.cafe" auth mask-src filter "rspam… | |
## this defines actions | |
# either deliver to lmtp or to an external server | |
action "local" lmtp "/var/dovecot/lmtp" alias <aliases> | |
action "outbound" relay | |
## this defines what should be done depending on some conditions | |
# receive emails (local or from external server for "puffy.cafe") | |
match from any for domain "puffy.cafe" action "local" | |
match from local for local action "local" | |
# send email (from local or authenticated user) | |
match from any auth for any action "outbound" | |
match from local for any action "outbound" | |
``` | |
In addition, you can configure the advertised hostname by editing the | |
file `/etc/mail/mailname`: for instance my machine's hostname is | |
`ryzen` so I need this file to advertise it as `mail.puffy.cafe`. | |
Restart OpenSMTPD with `rcctl restart smtpd`. | |
### TLS | |
For ports using STARTTLS (25 and 587), there are different options with | |
regard to TLS encryption. | |
* do not allow STARTTLS | |
* offer STARTTLS but allow not using it (option `tls`) | |
* require STARTTLS: drop connection when the remote peer does ask for | |
STARTTLS (option `tls-require`) | |
* require STARTTLS: drop connection when no STARTTLS, and verify the | |
remote certificate (option `tls-require verify`) | |
It is recommended to enforce STARTTLS on port 587 as it is used by | |
authenticated users to send emails, preventing them to send emails | |
without network encryption. | |
On port 25, used by external servers to reach yours, it is important to | |
allow STARTTLS because most server will deliver emails over an | |
encrypted TLS session, however it is your choice to enforce it or not. | |
Enforcing STARTTLS might break email delivery from some external | |
servers that are outdated or misconfigured (or bad actors). | |
### User management | |
By default, OpenSMTPD is configured to deliver email to valid users in | |
the system. In my example, if user `solene` exists, then email address | |
`[email protected]` will deliver emails to `solene` user mailbox. | |
Of course, as you do not want the system daemons to receive emails, a | |
file contains aliases to redirect emails from a user to another, or | |
simply discard it. | |
In `/etc/mail/aliases`, you can redirect emails to your username by | |
adding a new line, in the example below I will redirect root emails to | |
my user. | |
``` | |
root: solene | |
``` | |
It is possible to redirect to multiple users using a comma to separate | |
them, this is handful if you want to create a local group delivering | |
emails to multiple users. | |
Instead of a user, it is possible to append the incoming emails to a | |
file, pipe them to a command or return an SMTP code. The aliases(5) | |
man pages contains all you need to know. | |
OpenBSD manual pages: aliases(5) | |
Every time you modify this file, you need to run the command `smtpctl | |
update table aliases` to reload the aliases table in OpenSMTPD memory. | |
You can add a new email account by creating a new user with a shell | |
preventing login: | |
``` | |
useradd -m -s /sbin/nologin username_here | |
passwd username_here | |
``` | |
This user will not be able to do anything on the server but connecting | |
to SMTP/IMAP/POP. They will not be able to change their password | |
either! | |
### Handling extra domains | |
If you need to handle emails for multiple domains, this is rather | |
simple: | |
* Add this line to the file `/etc/mail/smtpd.conf` by changing | |
`puffy.cafe` to the other domain name: `match from any for domain | |
"puffy.cafe" action "local"` | |
* Configure the other domain DNS MX/SPF/DKIM/DMARC | |
* Configure `/etc/rspamd/local.d/dkim_signing.conf` to add a new block | |
with the other domain, the dkim selector and the dkim key path | |
* The PTR does not need to be modified as it should match the machine | |
hostname advertised over SMTP, and it is an unique value anyway | |
If you want to use a different aliases table for the other domain, you | |
need to create a new aliases file and configure `/etc/mail/smtpd.conf` | |
accordingly where the following lines should be added: | |
``` | |
table lambda file:/etc/mail/aliases-lambda | |
action "local_mail_lambda" lmtp "/var/dovecot/lmtp" alias <lambda> | |
match from any for domain "lambda-puffy.eu" action "local_mail_lambda" | |
``` | |
Note that the users will be the same for all the domains configured on | |
the server. If you want to have separate users per domains, or that | |
"user a" on domain A and "user a" on domain B could be different | |
persons / logins, you would need to setup virtual users instead of | |
using system users. Such setup is beyond the scope of this guide. | |
### Without Dovecot | |
It is possible to not use Dovecot. Such setup can suit users who would | |
like to download the maildir directory using rsync on their local | |
computer, this is a one-way process and does not allow sharing a | |
mailbox across multiple devices. This reduces maintenance and attack | |
surface at the cost of convenience. | |
This may work as a two-way access (untested) when using a software such | |
as unison to keep both the local and remote directories synchronized, | |
but be prepared to manage file conflicts! | |
If you want this setup, replace the following line in smtpd.conf | |
``` | |
action "local" lmtp "/var/dovecot/lmtp" alias <aliases> | |
``` | |
by this line: if you want to store the emails into a maildir format (a | |
directory per email folder, a file per email), emails will be stored in | |
the directory "Maildir" in user's homes. | |
``` | |
action "local" maildir "~/Maildir/" junk alias <aliases> | |
``` | |
or this line if you want to keep the mbox format (a single file with | |
emails appended to it, not practical), the emails will be stored in | |
/var/mail/$user. | |
``` | |
action "local" mbox alias <aliases> | |
``` | |
Wikipedia page: Maildir format | |
Wikipedia page: Mbox format | |
## Dovecot | |
Dovecot is an important piece of software for the domain end users, it | |
provides protocols like IMAP or POP3 to read emails from a client. It | |
is the most popular open source IMAP/POP server available (the other | |
being Cyrus IMAP). | |
Install dovecot with the following command line: | |
``` | |
pkg_add dovecot-- dovecot-pigeonhole-- | |
``` | |
Dovecot has a lot of configuration files in `/etc/dovecot/conf.d/` | |
although most of them are commented and ready to be modified, you will | |
have to edit a few of them. This guide provides the content of files | |
with empty lines / comments stripped so you can quickly check if your | |
file is ok, you can use the command `awk '$1 !~ /^#/ && $1 ~ /./'` on a | |
file to display its "useful" content only (awk will not modify the | |
file). | |
Modify `/etc/dovecot/conf.d/10-ssl.conf` and search the lines | |
`ssl_cert` and `ssl_key`, change their values to your certificate full | |
chain and private key. | |
Generate a Diffie-Hellman file for perfect forward secrecy, this will | |
make each TLS negociation unique, so if the private key ever leak, | |
every past TLS communication will remain safe. | |
```shell | |
openssl dhparam -out /etc/dovecot/dh.pem 4096 | |
chown _dovecot:_dovecot /etc/dovecot/dh.pem | |
chmod 400 /etc/dovecot/dh.pem | |
``` | |
The file (filtered of all comments/empty lines) should look like the | |
following: | |
```dovecot | |
ssl_cert = </etc/ssl/mail.puffy.cafe.fullchain.pem | |
ssl_key = </etc/ssl/private/mail.puffy.cafe.key | |
ssl_dh = </etc/dovecot/dh.pem | |
``` | |
Modify `/etc/dovecot/conf.d/10-mail.conf`, search for a commented line | |
`mail_location`, uncomment it and set the value to `maildir:~/Maildir`, | |
this will tell Dovecot where users mailboxes are stored and in which | |
format, we want to use the maildir format. | |
The resulting file should look like: | |
```dovecot | |
mail_location = maildir:~/Maildir | |
namespace inbox { | |
inbox = yes | |
} | |
mmap_disable = yes | |
first_valid_uid = 1000 | |
mail_plugin_dir = /usr/local/lib/dovecot | |
protocol !indexer-worker { | |
} | |
mbox_write_locks = fcntl | |
``` | |
Modify the file `/etc/dovecot/conf.d/20-lmtp.conf`, LMTP is the | |
protocol used by opensmtpd to transmit incoming emails to dovecot. | |
Search for the commented variable `mail_plugins` and uncomment it with | |
the value `mail_plugins = $mail_plugins sieve`: | |
The resulting file should look like: | |
```dovecot | |
protocol lmtp { | |
mail_plugins = $mail_plugins sieve | |
} | |
``` | |
If you do not want to use IMAP or POP3, you do not need Dovecot. There | |
is an explanation above how to proceed without Dovecot. | |
### IMAP | |
Wikipedia page: IMAP protocol | |
IMAP is an efficient protocol that returns headers of emails per | |
directory, so you do not have to download all your emails to view the | |
directory list, emails are downloaded upon read (by default in most | |
email clients). It allows some cool features like server side search, | |
incoming email sorting with sieve filters or multi devices access. | |
Edit `/etc/dovecot/conf.d/20-imap.conf` and configure the last lines | |
accordingly to the result file: | |
```dovecot | |
protocol imap { | |
mail_plugins = $mail_plugins imap_sieve | |
mail_max_userip_connections = 25 | |
} | |
``` | |
The number of connections per user/IP should be high if you have an | |
email client tracking many folders, in IMAP a connection is required | |
for each folder, so the number of connections can quickly increase. On | |
top of that, if you have multiple devices under the same public IP you | |
could quickly reach the limit. I found 25 worked fine for me with 3 | |
devices. | |
### POP | |
Wikipedia page: POP protocol | |
POP3 is a pretty old protocol that is rarely considered by users, I | |
still consider it a viable alternative to IMAP depending on your needs. | |
A major incentive for using POP is that it downloads all emails locally | |
before removing them from the server. As we have no tooling to encrypt | |
emails stored on remote email servers, POP3 is a must if you want to | |
not leave any email on the server. POP3 does not support remote | |
folders, so you can not use Sieve filters on the server to sort your | |
emails and then download them as-this. A POP3 client downloads the | |
Inbox and then sorts the emails locally. | |
It can support multiple devices under some conditions: if you delete | |
the emails after X days, your devices should synchronize before the | |
emails are removed. In such case they will have all the emails stored | |
locally, but they will not be synced together: if both computers A and | |
B are up-to-date, when deleting an email on A, it will still be in B. | |
There are no changes required for POP3 in Dovecot as the defaults are | |
good enough. | |
### JMAP | |
For information, a replacement for IMAP called JMAP is in development, | |
it is meant to be better than IMAP in every way and also include | |
calendars and address book management. | |
JMAP Implementations are young but exist, although support in email | |
clients is almost non-existent. For instance, it seems Mozilla | |
Thunderbird is not interested in it, an issue in their bug tracker | |
about JMAP from December 2016 only have a couple of comments from | |
people who would like to see it happening, nothing more. | |
Issue 1322991: Add support for new JMAP protocol | |
From the JMAP website page listing compatible clients, I only | |
recognized the name "aerc" which is a modern console email client. | |
JMAP project website: clients list | |
### Sieve (filtering rules) | |
Wikipedia page: Sieve | |
Dovecot has a plugin to offer Sieve filters, they are rules applied to | |
received emails going into your mailbox, whether you want to sort them | |
into dedicated directories, mark them read or block some addresses. | |
That plugin is called pigeonhole. | |
You will need Sieve to enable the spam filter learning system when | |
moving emails from/to the Junk folder as it is triggered by a Sieve | |
rule. This improves rspamd Bayes (a method using tokens to understand | |
information, the story of the person behind it is interesting) filter | |
ability to detect spam accurately. | |
Edit `/etc/dovecot/conf.d/90-plugin.conf` with the following content: | |
``` | |
plugin { | |
sieve_plugins = sieve_imapsieve sieve_extprograms | |
# From elsewhere to Spam folder | |
imapsieve_mailbox1_name = Spam | |
imapsieve_mailbox1_causes = COPY | |
imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sie… | |
# From Spam folder to elsewhere | |
imapsieve_mailbox2_name = * | |
imapsieve_mailbox2_from = Spam | |
imapsieve_mailbox2_causes = COPY | |
imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve | |
sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve | |
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment | |
} | |
``` | |
This piece of configuration was taken from the official Dovecot | |
documentation: | |
https://doc.dovecot.org/configuration_manual/howto/antispam_with_sieve/ | |
. It will trigger shell scripts calling rspamd to make it learn what | |
does a spam look like, and what is legit (ham). One script will run | |
when an email is moved out of the spam directory (ham), another one | |
when an email is moved to the spam directory (spam). | |
Modify `/etc/dovecot/conf.d/15-mailboxes.conf` to add the following | |
snippet inside the block `namespace inbox { ... }`, it will associate | |
the Junk directory as the folder containing spam and automatically | |
create it if it does not exist: | |
``` | |
mailbox Spam { | |
auto = create | |
special_use = \Junk | |
} | |
``` | |
To make this work completely, you need to write the two extra sieve | |
filters that will run trigger the scripts: | |
Create `/usr/local/lib/dovecot/sieve/report-spam.sieve` | |
```sieve | |
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; | |
if environment :matches "imap.user" "*" { | |
set "username" "${1}"; | |
} | |
pipe :copy "sa-learn-spam.sh" [ "${username}" ]; | |
``` | |
Create `/usr/local/lib/dovecot/sieve/report-ham.sieve` | |
```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 "sa-learn-ham.sh" [ "${username}" ]; | |
``` | |
Create `/usr/local/lib/dovecot/sieve/sa-learn-ham.sh` | |
```shell | |
#!/bin/sh | |
exec /usr/local/bin/rspamc -d "${1}" learn_ham | |
``` | |
Create `/usr/local/lib/dovecot/sieve/sa-learn-spam.sh` | |
```shell | |
#!/bin/sh | |
exec /usr/local/bin/rspamc -d "${1}" learn_spam | |
``` | |
Make the two scripts executable with `chmod +x | |
/usr/local/lib/dovecot/sieve/sa-learn-spam.sh | |
/usr/local/lib/dovecot/sieve/sa-learn-ham.sh`. | |
Run the following command to compile the sieve filters: | |
``` | |
sievec /usr/local/lib/dovecot/sieve/report-spam.sieve | |
sievec /usr/local/lib/dovecot/sieve/report-ham.sieve | |
``` | |
### Manage Sieve | |
By default, Sieves rules are a file located on the user home directory, | |
however there is a standard protocol named "managesieve" to manage | |
Sieve filters remotely from an email client. | |
It is enabled out of the box in Dovecot configuration, although you | |
need to make sure you open the port 4190/tcp in the firewall if you | |
want to allow users to use it. | |
### Start the service | |
Once you configured everything, make sure that dovecot service is | |
enabled, and then start / restart it: | |
``` | |
rcctl enable dovecot | |
rcctl start dovecot | |
``` | |
# Webmail | |
A webmail will allow your users to read / send emails from a web | |
interface instead of having to configure a local email client. While | |
they can be convenient, they enable a larger attack surface and are | |
often affected by vulnerability issues, you may prefer to avoid webmail | |
on your server. | |
The two most popular open source webmail are Roundcube mail and | |
Snappymail (a fork of the abandoned rainloop) and Roundcube, they both | |
have pros and cons. | |
## Roundcube mail setup | |
Roundcube is packaged in OpenBSD, it will pull in all required | |
dependencies and occasionally receive backported security updates. | |
Install the package: | |
``` | |
pkg_add roundcubemail | |
``` | |
When installing the package, you will be prompted for a database | |
backend for PHP. If you have one or two users, I highly recommend | |
choosing SQLite as it will work fine without requiring a running | |
daemon, thus less maintenance and server resources locked. If you plan | |
to have a lot of users, there are no wrong picks between MySQL or | |
PostgreSQL, but if you already have one of them running it would be | |
better to reuse it for Roundcube. | |
Specific instructions for installing Roundcube are provided by the | |
package README in `/usr/local/share/doc/pkg-readmes/roundcubemail`. | |
We need to enable a few PHP modules to make Roundcube mail working: | |
``` | |
ln -s /etc/php-8.2.sample/zip.ini /etc/php-8.2/ | |
ln -s /etc/php-8.2.sample/intl.ini /etc/php-8.2/ | |
ln -s /etc/php-8.2.sample/opcache.ini /etc/php-8.2/ | |
ln -s /etc/php-8.2.sample/pdo_sqlite.ini /etc/php-8.2/ | |
``` | |
Note that more PHP modules may be required if you enable extra features | |
and plugins in Roundcube. | |
PHP is ready to be started: | |
``` | |
rcctl enable php82_fpm | |
rcctl start php82_fpm | |
``` | |
Add the following blocks to `/etc/httpd.conf`, make sure you opened the | |
port 443/tcp in your `pf.conf` and that you reloaded it with `pfctl -f | |
/etc/pf.conf`: | |
``` | |
server "mail.puffy.cafe" { | |
listen on egress tls | |
tls key "/etc/ssl/private/mail.puffy.cafe.key" | |
tls certificate "/etc/ssl/mail.puffy.cafe.fullchain.pem" | |
root "/roundcubemail" | |
directory index index.php | |
location "*.php" { | |
fastcgi socket "/run/php-fpm.sock" | |
} | |
} | |
types { | |
include "/usr/share/misc/mime.types" | |
} | |
``` | |
Restart httpd with `rcctl restart httpd`. | |
You need to configure Roundcube to use a 24 bytes security key and | |
configure the database: edit the file | |
`/var/www/roundcubemail/config/config.inc.php`: | |
Search for the variable `des_key`, replace its value by the output of | |
the command `tr -dc [:print:] < /dev/urandom | fold -w 24 | head -n 1` | |
which will generate a 24 byte random string. If the string contains a | |
quote character, either escape this character by prefixing it with a | |
`\` or generate a new string. | |
For the database, you need to search the variable `db_dsnw`. | |
If you use SQLite, change this line | |
``` | |
$config['db_dsnw'] = 'sqlite:///roundcubemail/db/sqlite.db?mode=0660'; | |
``` | |
by this line: | |
``` | |
$config['db_dsnw'] = 'sqlite:///db/sqlite.db?mode=0660'; | |
``` | |
If you chose MySQL/MariaDB or PostgreSQL, modify this line: | |
``` | |
$config['db_dsnw'] = 'mysql://roundcube:pass@localhost/roundcubemail'; | |
``` | |
by | |
``` | |
$config['db_dsnw'] = 'mysql://USER:PASSWORD@DATABASE_NAME'; | |
``` | |
Where `USER`, `PASSWORD` and `DATABASE_NAME` must match a new user and | |
database created into the backend. | |
Because PHP is chrooted on OpenBSD and that the OpenSMTPD configuration | |
enforces TLS on port 587, it is required to enable TLS to work in the | |
chroot: | |
``` | |
mkdir -p /var/www/etc/ssl | |
cp -p /etc/ssl/cert.pem /etc/ssl/openssl.cnf /var/www/etc/ssl/ | |
``` | |
To make sure the files `cert.pem` and `openssl.cnf` stay in sync after | |
upgrades, add the two commands to a file `/etc/rc.local` and make this | |
file executable. This script always starts at boot and is the best | |
place for this kind of file copy. | |
If your IMAP and SMTP hosts are not on the same server where Roundcube | |
is installed, adapt the variables `imap_host` and `smtp_host` to the | |
server name. | |
If Roundcube mail is running on the same server where OpenSMTPD is | |
running, you need to disable certificate validation because `localhost` | |
will not match the certificate and authentication will fail. Change | |
`smtp_host` line to `$config['smtp_host'] = 'tls://127.0.0.1:587';` and | |
add this snippet to the configuration file: | |
``` | |
$config['smtp_conn_options'] = array( | |
'ssl' => array('verify_peer' => false, 'verify_peer_name' => false), | |
'tls' => array('verify_peer' => false, 'verify_peer_name' => false)); | |
``` | |
From here, Roundcube mail should work when you load the domain | |
configured in `httpd.conf`. | |
For a more in-depth guide to install and configure Roundcube mail, | |
there is an excellent guide available which was written by Bruno | |
Flückiger: | |
Install Roundcube on OpenBSD | |
# Hardening | |
It is always possible to improve the security of this stack, all the | |
following settings are not mandatory, but they can be interesting | |
depending on your needs. | |
## Always allow the sender per email or domain | |
It is possible to configure rspamd to force it to accept emails from a | |
given email address or domain, bypassing the anti-spam. | |
To proceed, edit the file `/etc/rspamd/local.d/multimap.conf` to add | |
this content: | |
``` | |
local_wl_domain { | |
type = "from"; | |
filter = "email:domain"; | |
map = "$CONFDIR/local.d/whitelist_domain.map"; | |
symbol = "LOCAL_WL_DOMAIN"; | |
score = -10.0; | |
description = "domains that are always accepted"; | |
} | |
local_wl_from { | |
type = "from"; | |
map = "$CONFDIR/local.d/whitelist_email.map"; | |
symbol = "LOCAL_WL_FROM"; | |
score = -10.0; | |
description = "email addresses that are always accepted"; | |
} | |
``` | |
Create the files `/etc/rspamd/local.d/whitelist_domain.map` and | |
`/etc/rspamd/local.d/whitelist_email.map` using the command `touch`. | |
Restart the service rspamd with `rcctl restart rspamd`. | |
The created files use a simple syntax, add a line for each entry you | |
want to allow: | |
* a domain name in `/etc/rspamd/local.d/whitelist_domain.map` to allow | |
the domain | |
* an email address in `/etc/rspamd/local.d/whitelist_email.map` to | |
allow this address | |
There is no need to restart or reload rspamd after changing the files. | |
Reusing the same technique can be done to block domains/addresses | |
directly in rspamd by giving a high positive score. | |
## Block bots | |
I published on my blog a script and related configuration to parse | |
OpenSMTPD logs and block the bad actors with PF. | |
2023-06-22 Ban scanners IPs from OpenSMTP logs | |
This includes an ignore file if you do not want some IPs to be blocked. | |
## Split the stack | |
If you want to improve your email setup security further, the best | |
method is to split each part into dedicated systems. | |
As dovecot is responsible for storing and exposing emails to users, | |
this component would be safer in a dedicated system, so if a component | |
of the email stack (other than dovecot) is compromised, the mailboxes | |
will not be exposed. | |
## Network attack surface reduction | |
If this does not go against usability of the email server users, I | |
strongly recommend limiting the publicly opened ports in the firewall | |
to the minimum: 25, 80, 465, 587. This would prevent attackers to | |
exploit any network related 0day or unpatched vulnerabilities of | |
non-exposed services such as Dovecot. | |
A VPN should be deployed to allow users to reach Dovecot services | |
(IMAP, POP) and other services if any. | |
SSH port could be removed from the public ports as well, however, it | |
would be safer to make sure your hosting provider offers a serial | |
access / VNC / remote access to the system because if the VPN stops | |
working, you will not be able to log in into the system using SSH to | |
debug it. | |
# Email client configuration | |
If everything was done correctly so far, you should have a complete | |
email stack fully functional. | |
Here are the connection information to use your service: | |
* IMAP/POP3/SMTP login: username on the remote system (the username | |
does not include the `@` part) | |
* IMAP/POP3/SMTP password: password of the remote system user | |
* IMAP/POP3 server: dovecot server hostname | |
* IMAP/POP3 port: 993 for IMAPS and 995 for POP3S (TLS is enabled) | |
* SMTP server: opensmtpd server hostname | |
* SMTP port: either 465 in SSL/TLS mode (encryption forced), or 587 in | |
STARTTLS mode (encryption not enforced depending on OpenSMTPD | |
configuration) | |
The webmail, if any, will be available at the address configured in | |
`httpd.conf`, using the same credentials as above. | |
# Verify the setup | |
There is an online service providing you a random email address to send | |
a test email to, then you can check the result on their website | |
displaying if the SPF, DKIM, DMARC and PTR records are correctly | |
configured. | |
www.mail-tester.com | |
The score you want to be displayed on their website is no least than | |
10/10. The service can report meaningless issues like "the email was | |
poorly formatted" or "you did not include an unsubscribe link", they | |
are not relevant for the current test. | |
While it used to be completely free last time I used it, I found it | |
would ask you to pay after three free checks if you do not want to wait | |
24h. It uses your public IP address for the limit. | |
# Maintenance | |
## Running processes | |
The following processes list should always be running: using a program | |
like monit, zabbix or reed-alert to notify you when they stop working | |
could be a good idea. | |
* dovecot | |
* httpd | |
* redis | |
* rspamd | |
* smtpd | |
## Certificates renewal | |
In addition, the TLS certificate should be renewed regularly as ACME | |
generated certificates are valid for a few months. Edit root crontab | |
with `crontab -e` as root to add this line: | |
``` | |
10 4 * * 0 -s acme-client mail.puffy.cafe && rcctl restart dovecot httpd smtpd | |
``` | |
This will try to renew the certificate for `mail.puffy.cafe` every | |
Sunday at 04h10 and upon renewal restart the services using the | |
certificate: dovecot, httpd and smtpd. | |
## All about logs | |
If you need to find some logs, here is a list of paths where to find | |
information: | |
* dovecot: `/var/log/maillog` | |
* httpd: `/var/log/daemon` for the daemon, access logs in | |
`/var/www/logs/access.log` and errors logs in `/var/www/logs/error.log` | |
* redis: `/var/log/daemon` | |
* rspamd: `/var/log/rspamd/rspamd.log` and its web UI on port 11334 | |
(only on localhost by default, a SSH tunnel can be handy) | |
* smtpd: `/var/log/maillog` | |
* roundcube: `/var/www/roundcubemail/logs/errors.log` and | |
`/var/www/roundcubemail/logs/sendmail.log` | |
A log rotation of the new logs can be configured in | |
`/etc/newsyslog.conf` with these lines (take only what you need): | |
```newsyslog | |
/var/log/rspamd/rspamd.log 600 7 500 * Z "pkill -USR1 … | |
/var/www/roundcubemail/logs/errors.log 600 7 500 * Z | |
/var/www/roundcubemail/logs/sendmail.log 600 7 500 * Z | |
``` | |
## Disk space | |
Finally, OpenSMTPD will stop delivering emails locally if the `/var` | |
partition has less than 4% of free disk space, be sure to monitor the | |
disk space of this partition otherwise you will not receive emails | |
anymore for a while before noticing something is wrong. | |
# Conclusion | |
Congratulations, you configured a whole email stack that will allow you | |
to send emails to the world, using your own domain and hardware. | |
Keeping your system up to date is important as you have network | |
services exposed to the wild Internet. | |
Even with a properly configured setup featuring SPF/DKIM/DMARC/PTR, it | |
is not guaranteed to not end in the spam directory of our recipients. | |
The IP reputation of your SMTP server also account, and so is the | |
domain name extension (I have a `.pw` domain which I learned too late | |
that it was almost always considered as spam because it is not | |
mainstream). |