| 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). |