Setup OpenSMTPD with Dovecot on OpenBSD

— 2687 words — 14 min

#openbsd  #opensmtpd  #email 

Table of Contents
  1. rDNS and FCrDNS
  2. Setup SPF
  3. Setup DKIM
  4. Setup DMARC
  5. Obtain a Lets Encrypt Certificate with ACME
  6. Setup rspamd
  7. Finally Setup OpenSMTPD
  8. Setup Dovecot
  9. Misc
    1. Manually Connect and Authenticate with SMTPS
    2. Email with a Wrong SPF Record

I assume you already installed OpenBSD. If you haven’t take a look at the official installation guide. The process is well documented. When you have a running machine with OpenSSH (I wrote some notes about hardening OpenSSH), you’re good to go. You’ll also need access to the DNS settings of your domain.

Alright, with a running OpenBSD system let’s start with setting up the correct DNS records for the domain I want to send mail. Let’s assume it’s example.com.

rDNS and FCrDNS🔗

You want a correct rDNS (reverse DNS) and FCrDNS (forward committed rDNS) record. This is an effective method to detect spam if one of these records is incorrect. For instance, compromised desktops behind residential connections will usually not have a “legitimate” looking rDNS and so it’s easy (for us and anybody else) to block mail from there. The same is true for compromised servers that were not intended to send email or have a different domain associated with them. Setting up these records is like the minimal amount of work you have to do as an operator to state that this machine is supposed to send email.

Let’s assume the IP address of your mailserver is 1.2.3.4 and you want to serve mails for example.com. Then you want meet the following conditions.

Define which server is supposed to handle mail for example.com with the MX record and create an A record that resolves to the IP address of that server. You should know where to do that, either on your own nameserver or whereever you bought your domain if you use their nameserver. When you did that, you can verify the records with dig.

$ dig +short mx example.com
10 mx.example.com.

$ dig +short a mx.example.com
1.2.3.4

Now set the correct rDNS so that the domain name that handles email is returned if the IP address of that sever is looked up. You usually do that somewhere in the interface of your provider that hosts your server. Then verify that the IP address resolves to the correct domain.

$ dig +short -x 1.2.3.4
mx.example.com.

$ dig +short mx.example.com
1.2.3.4

When the mailserver accepts a connection from an IP address, it does a reverse DNS lookup on that IP. In this case, the result is the domain mx.example.com. Then this domain gets resolved and returns an IP address. If that resolved IP address and the connecting IP address are the same, you’re passing the Forward Committed rDNS check.

Setup SPF🔗

SPF is the Sender Policy Framework. It lets you define which IP addresses and/or hostnames are allowed to send email for your domain.

The following TXT record specifies that only the hostname defined in the MX record is allowed to send mail for the domain example.com.

v=spf1 mx -all

You can use dig again to verify the record.

$ dig +short txt example.com
"v=spf1 mx -all"

If a mail server receives an email and the sending mail server is not a hostname or IP address specified in the SPF record, then the -all part tells the receiving mail server to reject the email.

Setup DKIM🔗

DKIM stands for DomainKey Identified Mail. It helps the receiving mail server to detect forged email by looking at the DKIM signature. Every email that leaves the legitimate mail server is signed and the receiving mail server can verify the signature by using the public key that is published as a DNS TXT record. Let’s setup it up.

First, generate a 1024 bit RSA key and extract the public key that will be published in a DNS TXT record. Also, adjust the permissions of the private key so that it is not world readable and set the owner group to _rspamd so that rspamd can read it. If you wonder why rspamd needs to be able to read the file, it’s because in this setup rspamd will perform the signing operation as you will see in the setup of OpenSMTPD.

mkdir /etc/mail/dkim
cd /etc/mail/dkim
openssl genrsa -out example.com.key 1024
openssl rsa -in example.com.key -pubout -out example.com.pub
chmod 440 example.com.key
chown root:_rspamd example.com.key

Now, create a DNS TXT record according to the scheme <selector>._domainkey.example.com. You can choose <selector> arbitrarily, for example, the date you created the RSA key. The content of the txt records looks like this with k=rsa and p=<public key>. One line; no spaces; no line breaks; values separated by a semicolon.

v=DKIM1;k=rsa;p=MIGfMA0GCS[...]EudMVZQIDAQAB;

You can display the record with dig. Replace <selector> with the value you chose.

dig +short <selector>._domainkey.example.com txt

Setup DMARC🔗

DMARC stands for Domain-based Message Authentication, Reporting and Conformance. It lets you define how a receiving mail server should handle emails that do not pass the SPF or DKIM check—provided that the receiving mail server checks DMARC, SPF, and DKIM. Additionally, it may also have other local policies that it follows. The DMARC record is another TXT DNS entry consisting of a few properties. For all properties see RFC 7489. Required tags are the version v=DMARC1 (must also be the first one) and the policy p.

Example:

v=DMARC1;p=quarantine;pct=100;rua=mailto:postmaster@example.com;

My understanding of DMARC is still a bit fuzzy. For instance, I think that a mail server receiving an email from a server that is not listed in the SPF record (containing a -all) will reject that email instantly. I’m not sure whether a DMARC record can override this and have the email quarantined, for example. I think it depends on how the local service that does these checks is configured. In theory, it should probably be

  1. check the DMARC record of the sending domain and act accordingly
  2. if there is no DMARC record, apply local policies.

However, I guess one could also configure the service in such as way that the local policies are always used. I need to figure this out at some point. Or if you know more, get in touch. As you’ll see below the service that is responsible for these checks is rspamd with the filter filter-rspamd in the OpenSMTPD configuration.

Obtain a Lets Encrypt Certificate with ACME🔗

Setup httpd for the Lets Encrypt challenges.

cp /etc/examples/httpd.conf /etc/

For a minimal configuration, remove the SSL configurations so it looks like this.

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

Copy the example ACME configuration.

cp /etc/examples/acme-client.conf /etc/

Then edit the configuration slightly to match your domain name.

domain mx.example.com {
    domain key "/etc/ssl/private/mx.example.com.key"
    domain full chain certificate "/etc/ssl/mx.example.com.fullchain.pem"
    sign with letsencrypt
}

Run the acme-client.

$ acme-client -v mx.example.com
[...]
acme-client: /etc/ssl/mx.example.com.fullchain.pem: created

You can add that command to cron to be executed weekly. If the certificate is valid for less than 30 days it will be renewed automatically.

Setup rspamd🔗

Rspamd can do many things. Here it is reponsible for assigning a spam score and signing ougoing emails (DKIM) among other things.

$ pkg_add rspamd redis opensmtpd-filter-rspamd

Tell rspamd which selector to use for DKIM signing in /etc/rspamd/local.d/dkim_signing.conf. This is the DKIM selector you could choose arbitrarily above.

# rspamd expects the username to contain the domain name too.
# in this setup, mail users *only* authenticate with their username.
allow_username_mismatch = true;

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

As already mentioned when creating the key file, make sure that the key is readable by the _rspamd group.

Enable the services.

rcctl enable redis
rcctl start redis
rcctl enable rspamd
rcctl start rspamd

The rest of this rspamd section is optional. If you’re following along, you can probably skip this part and go straight to the Setup OpenSMTPD section. You can come back later and check if you need or want this.

The steps below add a new setting block called outgoing to rspamd that you can use in the OpenSMTPD configuration to adjust the behaviour of rspamd.

This outgoing setting block enables DKIM signing and sets the values to mark an email as spam to 100, so basically never. I use this specific rspamd configuration only for my outgoing mail, so that rspamd does not add its spam headers but still signs the emails (DKIM).

Why?
I did this because when user cron jobs send me mail (from @localhost to @localhost) it got flagged as spam. Mail from the root user seemed to be fine, though.

Add the new setting block in /etc/rspamd/local.d/settings.conf.

outgoing {
    id = "outgoing";
    apply {
        groups_enabled = ["dkim"];
        actions {
            reject = 100.0;
            greylist = 100.0;
            "add header" = 100.0;
        }
    }
}

Now I can use the -settings-id flag to tell filter-rspamd to use the new configuration block for outgoing mail and use the default configuration for incoming mail (notably, try to detect spam). Adjust the following in /etc/mail/smtpd.conf.

filter rspamd-in proc-exec "filter-rspamd"
filter rspamd-out proc-exec "filter-rspamd -settings-id outgoing"

# Then apply the filters to your incoming/outgoing listeners
listen on all filter "rspamd-in"
listen on all port submission filter "rspamd-out"

See filter-rspamd documentation on Github.

Finally Setup OpenSMTPD🔗

As OpenSMTPD is installed by default, you can just adjust its configuration in /etc/mail/smtpd.conf.

In English the configuration does roughly the following

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

table aliases file:/etc/mail/aliases

filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \
    disconnect "550 no residential connections"

filter check_rdns phase connect match !rdns \
    disconnect "550 no rDNS"

filter check_fcrdns phase connect match !fcrdns \
    disconnect "550 no FCrDNS"

filter rspamd proc-exec "filter-rspamd"

listen on all tls pki mx.example.com hostname mx.example.com \
    filter { check_dyndns, check_rdns, check_fcrdns, rspamd }

listen on all smtps pki mx.example.com auth mask-src filter rspamd

action "local_mail" maildir junk alias <aliases>
action "outbound" relay helo mx.example.com

match from any for domain "example.com" action "local_mail"
match for local action "local_mail"

match from any auth for any action "outbound"
match for any action "outbound"

I would advice to not copy and paste this configuration. It will work though, if you adjust the domain names and so on. Read the manpage of smtpd.conf(5) and familiarize yourself the options—at least the options you find in the configuration above.

However, I do want you to note the mask-src option on the listen line for email submission. If you don’t specify it, the first Received header in the email will contain a line such as the following:

Received: from [127.0.0.1] (dynamic-a-b-c-d.x.y.pool.isp.com
[a.b.c.d])
by localhost (OpenSMTPD) with ESMTPSA id d6b22206
(TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO)

As you can see, it recorded where that email orginated from: my residential IP address (that I manually marked as a.b.c.d) and the reverse DNS name. If you do specify mark-src this part is simply omitted in the email headers.

Received:
        by localhost (OpenSMTPD) with ESMTPSA id 9a19d954
        (TLSv1.3:TLS_AES_256_GCM_SHA384:256:NO)

You can do a lot with OpenSMTPD. You could create another table(5) referencing a file containing the regex expressions matching the dynamic rDNS of residential IP addresses (similar to the aliases table above), serve mails for multiple domains, create a users table that’s used instead of the normal system users in /etc/passwd and much more. Again, check out the manpage smtpd.conf(5) 🙂

Setup Dovecot🔗

Dovecot provides the imap service so that I can connect my phone or other mail clients that use imap to read and download mail. First, install the package.

pkg_add dovecot

Then I tweaked the following configuration settings.

Disable plain text authentication in /etc/dovecot/conf.d/10-auth.conf.

disable_plaintext_auth = yes

Require an SSL connection and tell dovecot which certificate and key file to use in /etc/dovecot/conf.d/10-ssl.conf.

ssl = required
ssl_cert = </etc/ssl/mx.example.com.fullchain.pem
ssl_key = </etc/ssl/private/mx.example.com.key

Tell dovecot where mail is located in /etc/dovecot/conf.d/10-mail.conf.

mail_location = maildir:~/Maildir

Only provide imaps on port 993. I don’t need pop3 nor do I need imap on port 143 via STARTTLS. This is configured in /etc/dovecot/dovecot.conf.

protocols = imaps

That’s it. Enable and start dovecot.

rcctl enable dovecot
rcctl start dovecot

Now I can connect my email clients via imaps using a user and password defined in /etc/passwd. You might be used to use your email address as user name but with this setup, you just use your local user on the server.

Misc🔗

Some miscellaneous things 🙂

Manually Connect and Authenticate with SMTPS🔗

If you’re connecting to smtp on port 25 it’s pretty easy.

nc -v example.com 25

And you can start talking smtp to the server. It’s a bit harder when you try to go through SMTPS on port 465

Let’s start with the password encoding used by the authentication method auth plain in smtp. The order is a null byte, the user name, a null byte, and the password encoded as base64.

$ echo -en '\0username\0password' | base64
AHVzZXJuYW1lAHBhc3N3b3Jk

Be aware of your shell history, if you do that 🙂.

Then I can use openssl to start a tls connection and greet the mail server. If I wanted to start sending mail, it tells me to authenticate first. So I type auth plain and send the base64 encoded combination of the username and password.

$ openssl s_client -connect mx.example.com:465
[...]
220 localhost ESMTP OpenSMTPD
helo u
250 localhost Hello u [a.b.c.d], pleased to meet you
mail from: <notthere@example.com>
530 5.5.1 Invalid command: Must issue an AUTH command first
auth plain
334
AHVzZXJuYW1lAHBhc3N3b3Jk
235 2.0.0 Authentication succeeded
[...]

Now I can submit my mail.

Email with a Wrong SPF Record🔗

Why? Because I wanted to see how the log entry looks like.

I set a wrong SPF record (meaning I chose a different IP address than the one from the real server) for the domain intentionally and then tried to send an email to another domain running postfix as smtp service. The error on the postfix side (I control that server, too) looks very similar to the error below which is logged by OpenSMTPD.

The email gets rejected with a PermFail (I replaced occurences of the other domain with somewhere.com)

Sep 26 20:11:50 hostname smtpd[40801]: 7bceeb8376da5b52 mta delivery evpid=43ace175217a562a from=<user@example.com> to=<somewhere@somewhere.com> rcpt=<-> source="1.2.3.4" relay="a.b.c.d (mail.somewhere.com)" delay=0s result="PermFail" stat="550 5.7.23 <somewhere@somewhere.com>: Recipient address rejected: Message rejected due to: SPF fail - not authorized. Please see http://www.openspf.net/Why?s=helo;id=example.com;ip=1.2.3.4;r=somewhere.com"

Basically, as far as postfix was concerned, somebody tried to spoof an email for the @example.com domain because when postfix looked up the SPF record it found a different IP than the one that was now trying to hand over an email. So it got rejected.

Found a mistake or have a suggestion? Get in touch!


Articles from blogs I follow around the net

Status update, June 2024

Hi all! This status update will be shorter than usual because I had a lot less free time for my open-source projects than usual this month. Indeed, I recently joined SNCF Réseau (the company responsible for the French railway infrastructure) to work on OSRD, …

via emersion June 19, 2024

Whose CIDR is it anyway?

A look at CIDR block ownership from a RIR-, country-, and organization level. Originally presented at RIPE88.

via Signs of Triviality June 12, 2024

How and why to make a /now page on your site

Background I used to wonder what my friend Benny Lewis was doing. He has a website and social media accounts, but neither gave an overview of what he’s doing now. Then I realized some people might wonder the same about me. So in 2015, I made a /now page on my…

via Derek Sivers blog May 18, 2024

Generated by openring