Configure OpenSMTPD (with Maildir)

In this guide, we provide a sample configuration of OpenSMTPd using Maildir. SPF, DKIM, and DMARC will also be configured.

Before we begin

Read the the man pages for opensmtpd, smtpd.conf, and smtpctl.

Also, check out the free OpenSMTPd book by the author of OpenSMTPd!

DNS

Running a mail server requires proper DNS records. If you have not already, you will want to read up on DNS and set up your name server.

You will need to add proper DNS records to your domain and make sure they work.

Install

Opensmtpd is part of OpenBSD base, but we will also want to install some opensmtpd-related packages:

$ doas pkg_add opensmtpd-extras opensmtpd-filter-dkimsign--

Make sure to read the dkimsign filter README at /usr/local/share/doc/pkg-readmes/opensmtpd-filter-dkimsign.

Configuration

TLS

You will want to use acme-client to request a TLS public cert and private key in /etc/ssl/.

Next, we'll create our smtpd configuration file in /etc/mail/smtpd.conf:

# PKI for TLS
pki example.com cert "/etc/ssl/example.com.crt"
pki example.com key "/etc/ssl/private/example.com.key"

This defines our public and private key pair for TLS encryption.

Tables

Next, we define 5 tables:

# tables setup
table domains file:/etc/mail/domains
table aliases file:/etc/mail/aliases
table hosts file:/etc/mail/hosts
table relayaddr file:/etc/mail/relayaddr
table users file:/etc/mail/users

The domains table contains a list of domains that our mail server should receive mail on.

Note: Do not add domains that your mail server does not directly serve (for example, do not add domains you intend to forward mail to). If you add them by mistake, the mail server will not forward the mail properly.

The aliases file helps handle mail forwarding. It is written using @@key: value@@ pairs. See aliases(5) for more information.

The hosts file contains a list of IP addresses that this current host will send email from.

The relayaddr file contains a list of trusted IP addresses this mail server is willing to relay mail for without authentication.

The users file contains a list of valid sending users.

All of these tables will be explained further in the following sections.

Dealing with Spam

# Blocks junk mail
filter check_rdns phase connect match !rdns junk
filter check_fcrdns phase connect match !fcrdns junk
filter "dkimsign" proc-exec "filter-dkimsign -d example.com -s mail -k /etc/mail/dkim/private.key" user _dkimsign group _dkimsign

The first filter will check if the sender has an rdns entry. If not, the mail will be labeled as junk and placed in ~/Maildir/.Junk/.

The second filter will check if the sender's forward and reverse dns entry match. If not, the mail will be labeled as junk and placed in ~/Maildir/.Junk/.

The third filter will sign any email with the DKIM private key.

  1. -d specifies the domain name to sign for; you must replace example.com with your real domain.
  2. -s specifies the selector (in this case mail).
  3. -k specifies the path of the private key.
  4. user and group both specify _dkimsign, the user and group that does the signing

Macros

A macro defines a variable that will be replaced with a block of text:

# macros
ipv4 = "192.168.0.1"
ipv6 = "2001:db8::"
optional = "pki example.com auth-optional mask-src senders <users> filter { check_rdns check_fcrdns } hostname example.com"
required = "pki example.com auth mask-src senders <users> filter { dkimsign } hostname example.com"

Lines 2 and 3 define the IPv4 and IPv6 addresses used that smtpd(8) will listen on, for receiving mail from other mail servers and from email clients.

Line 4 tells opensmtpd to use the public/private keys we defined earlier for example.com. We optionally allow authentication using normal login credentials, for users that want to send email using port 25. Authentication helps to avoid an open mail relay. Typically, login credentials are handled by the passwd(5) file.

We check if the user is allowed to send email as the specified user by consulting the users table stored in /etc/mail/users. We then mask the sender's source (the from part of the Received header). We also apply two filters to check for proper forward and reverse confirmed DNS entries. Finally, we indicate that the sending hostname must be example.com instead of the default server name.

Line 5 is identical to line 4 except it requires authentication.

Listeners

The listeners tell us what network interfaces, IP addresses, and ports to listen on.

# listeners
listen on socket filter "dkimsign"
listen on lo0 filter "dkimsign"
listen on $ipv4 port 25 tls $optional
listen on $ipv6 port 25 tls $optional
listen on $ipv4 port 465 smtps $required
listen on $ipv6 port 465 smtps $required
listen on $ipv4 port 587 tls-require $required
listen on $ipv6 port 587 tls-require $required

Line 2 tells smtpd to listen to the UNIX domain socket and to DKIM sign all emails. Line 3 tells us to listen to the loopback interface and also sign all emails.

Lines 4-5 tells smtpd to listen on the IPv4 and IPv6 address on port 25, to provide TLS if supported but to offer plaintext as a fallback. Authentication is optionally offered if the client wants to send email.

Lines 6-7 tells smtpd to listen on the IPv4 and IPv6 address on port 465, for SMTPS. TLS encryption is required and authentication checking is forced. This socket will be used for sending mail to other servers.

Lines 8-9 is similar except it's for port 587, which is the SMTP submission port.

Rules

Next we define the actions that opensmtpd can take and how to decide which action to follow:

# rules
action "maildir" maildir "%{user.directory}/Maildir" junk alias <aliases>
action "outbound" relay src <hosts>

match from any for domain <domains> action "maildir"
match from local for any action "outbound"
match from src <relayaddr> for any action "outbound"
match auth from any for any action "outbound"

In line 2, we define the action "maildir": mail destined for our domain will be delivered to the user's ~/Maildir folder.

In line 3, we define the action "outbound": we relay (send) the email out.

Line 4 defines our first matching rule: any email headed for one of our domains should be handed over to action maildir.

Line 5 defines our second matching rule: any email from a local IP address or queue can relay (send) without authentication.

Line 6 defines our third matching rule: any email from a trusted relay address in the relayaddr table (defined in /etc/mail/relayaddr) can be relayed (sent) without authentication.

Line 7 defines our last matching rule: any email that has been properly authenticated will be relayed (sent).

Complete configuration file

Here is the entire configuration file in /etc/mail/smtpd.conf:

# PKI for TLS
pki example.com cert "/etc/ssl/example.com.crt"
pki example.com key "/etc/ssl/private/example.com.key"

# tables setup
table domains file:/etc/mail/domains
table aliases file:/etc/mail/aliases
table hosts file:/etc/mail/hosts
table relayaddr file:/etc/mail/relayaddr
table users file:/etc/mail/users

# Blocks junk mail
filter check_rdns phase connect match !rdns junk
filter check_fcrdns phase connect match !fcrdns junk
filter "dkimsign" proc-exec "filter-dkimsign -d example.com -s mail -k /etc/mail/dkim/private.key" user _dkimsign group _dkimsign

# macros
ipv4 = "192.168.0.1"
ipv6 = "2001:db8::"
optional = "pki example.com auth-optional mask-src senders <users> filter { check_rdns check_fcrdns } hostname example.com"
required = "pki example.com auth mask-src senders <users> filter { dkimsign } hostname example.com"

# listeners
listen on socket filter "dkimsign"
listen on lo0 filter "dkimsign"
listen on $ipv4 port 25 tls $optional
listen on $ipv6 port 25 tls $optional
listen on $ipv4 port 465 smtps $required
listen on $ipv6 port 465 smtps $required
listen on $ipv4 port 587 tls-require $required
listen on $ipv6 port 587 tls-require $required

# rules
action "maildir" maildir "%{user.directory}/Maildir" junk alias <aliases>
action "outbound" relay src <hosts>

match from any for domain <domains> action "maildir"
match from local for any action "outbound"
match from src <relayaddr> for any action "outbound"
match auth from any for any action "outbound"

Adding users

For each new user, add a line such as the following in /etc/mail/aliases:

username:	username

To handle email forwarding, add this line:

username:	username@mail.example

You can ensure any mail sent to root gets forwarded to your username and external email address:

root:	username, username@mail.example

Now, any mail sent to root will get forwarded to username's Maildir and also to username@mail.example

NOTE: Make sure to check the mail account linked to root often! daily(8) and other programs will send mails to root.

You'll also need to create one line for each user in /etc/mail/users:

username:	username@example.com

The mail server's public IP addresses used to send email should go into /etc/mail/hosts:

192.168.1.1
2001:db8::

Replace IP addresses 192.168.1.1 and 2001:db8:: with your server's real IP addresses.

In /etc/mail/mailname, put in the name you want to use for your mail server. This is very important for passing anti-spam checks:

example.com

The list of domains this mail server can receive emails for will go inside /etc/mail/domains:

example.com
mail.example.com

Authentication is handled by normal login credentials. Most likely, this means passwords can be set with passwd(1):

$ passwd $USERNAME

WARNING: Special characters like $, when used in passwords, may cause issues with your mail client or with opensmtpd. To be safe, you might want to use only alphanumeric characters for your password. You can increase the length of the password for more security.

File Permissions

Make sure to set the proper permissions:

$ doas chown -R _dkimsign:_dkimsign /etc/mail/dkim/
$ doas chown _smtpd:_dovecot /etc/mail/passwd
$ doas chmod 770 /etc/mail/dkim/
$ doas chmod 440 /etc/mail/passwd
$ doas find /etc/mail ! -path /etc/mail -exec chmod o-rwx '{}' +

Note that you want to keep the ownership of any files that are listed in /etc/mtree/special the same, and the file permissions must be at least as strict as those. Otherwise the security(8) script run by daily(8) will flag those files and mail you about them.

IMAP and POP3 via dovecot

If desired, you can optionally configure opensmtpd to use LMTP and install and configure dovecot.

DKIM signing

We will need to set up dkim to have the mail properly signed.

Troubleshooting

OpenSMTPD may end up in an inconsistent state. This can happen due to a misconfiguration. One symptom is you see this error:

smtpd[]: pony express: smtpd: socket: Too many open files

To fix this, you can delete all the temporary files inside OpenSMTPD.

WARNING: this will delete any messages in the queue:

$ doas rcctl stop smtpd
$ doas rm -r /var/spool/smtpd/queue/*
$ doas rm -r /var/spool/smtpd/offline/*

At times, opensmtpd may be unable to connect because outgoing packets are being filtered. For example, suppose you are trying to send a letter to yahoo, but you get errors similar to following, showing a connection timeout:

smtpd[]: smtp-out: Enabling route [] <-> 67.195.204.77 (mtaproxy1.free.mail.vip.bf1.yahoo.com)
smtpd[]: smtp-out: Enabling route [] <-> 67.195.228.106 (mtaproxy2.free.mail.vip.gq1.yahoo.com)
smtpd[]: mta error reason=Connection timeout
smtpd[]: smtp-out: Disabling route [] <-> 104.47.55.33 (104.47.55.33) for 15s

An easy way to test if your packets are being filtered is:

$ dig -t mx yahoo.com
;; ANSWER SECTION:
yahoo.com.              395     IN      MX      1 mta6.am0.yahoodns.net.
yahoo.com.              395     IN      MX      1 mta5.am0.yahoodns.net.
yahoo.com.              395     IN      MX      1 mta7.am0.yahoodns.net.
$ nc mta5.am0.yahoodns.net 25

If you get no response, then outgoing packets to port 25 are being blocked (often due to firewalls by your VPS provider to block spam). If mail is working, you should see a 220 reply:

$ nc mta5.am0.yahoodns.net 25
220 mtaproxy511.free.mail.ne1.yahoo.com ESMTP ready

It is also possible that TLS is being dropped by the firewall. You can test using openssl:

$ openssl s_client -starttls smtp -connect mta5.am0.yahoodns.net:25
CONNECTED(00000003)
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 High Assurance Server CA
verify return:1
depth=0 C = US, ST = California, L = Sunnyvale, O = Oath Inc, CN = *.am0.yahoodns.net
...
250 STARTTLS

You should see the entire SSL cert plus 250 STARTTLS reply. If you see the response hang at any point (eg, it returns CONNECTED(00000003) and nothing else), then TLS on port 25 is being filtered.

If you see this warning message in /var/log/maillog:

Dec  6 03:44:17 smtpd[]: info: OpenSMTPD 6.7.0 starting                                 
Dec  6 03:44:17 smtpd[]: pony express: smtpd: socket: Too many open files               
Dec  6 03:44:17 smtpd[]: warn: lost child: pony express exited abnormally               

This is due to having too many IP addresses that opensmtpd tries to bind to. This happens when you have a rule that says listen on egress:

listen on egress port 25 tls pki example.com $optional
listen on egress port 587 tls-require pki example.com $required

These two lines mean that opensmtpd will listen to all available ip addresses, including the hundreds of IPv6 addresses you may have in /etc/hostname.vio0 and ifconfig vio0. To fix this, you must specify the exact IP addresses you want to listen to.

Open Mail Relay

If all your email is being marked as spam, check /var/log/maillog . If you see a message like the following:

Jan  8 11:00:29 smtpd[39035]: 83bd6b3b1669649f mta delivery evpid=a8d16cd2144222fa from=<spammer@example.com> to=<victim@example.com> rcpt=<-> source="192.168.0.1" relay="10.0.0.1 (10.0.0.1)" delay=16h2s result="TempFail" stat="451 4.7.650 The mail server [192.168.0.1] has been temporarily rate limited due to IP reputation. For e-mail delivery information, see https://postmaster.example.com (S843)"

Then your server may be exploited by a spammer. You may need to check if you are running an open mail relay! Please follow the guide to fix it.

Troubleshooting OpenSMTPd