How To... | Why not..? | Scripts | Patches |
Last update: 2024-04-03
Yes, another post about setting up a mail server. I know, there are plenty similar posts already out there… This one is about setting up a mail server on an Internet facing host. It will accept and send mails for a domain, store the accepted mails locally and deliver them using POP3. This is a rather lengthy post because there are quite some pieces to put together.
I use the following software to build my mail server out in the wild Internet:
OpenSMTPD will handle incoming and outgoing mail using SMTP. rspamd will support it in fighting incoming spam and malware (using ClamAV), and signing outgoing mail using DKIM. Dovecot stores received mail for users and delivers it using POP3. Finally acme-client(8) is used to manage the certificate from Let’s Encrypt.
The configuration I describe in this post is based on some assumptions about the server and its environment:
This setup is suited for a small domain providing mail services to few users. User management is done manually and mail is stored locally. If this doesn’t fit your needs you might consider using one of the many other mail server guides out there.
Before you start installing and configuring any software on your OpenBSD system I suggest that you consider using full disk encryption on it. Especially if your mail server is hosted at some provider.
First you should install the required software packages:
$ doas pkg_add -i dovecot opensmtpd-extras rspamd opensmtpd-filter-rspamd
Next you create the system user which will be used for handling mails:
$ doas useradd -c "Virtual Mail" -d /home/vmail -g =uid -s $(which nologin) vmail
$ doas mkdir /home/vmail
$ doas chown vmail:vmail /home/vmail
$ doas chmod 0750 /home/vmail
And you create the passwd(5) which will hold the information about the mail users on your system:
$ smtpctl encrypt 1amApASSw0rd | sed "s/^/muser:/;s/$/::::::/" > passwd
$ doas mv passwd /etc/mail/passwd
$ doas chown _dovecot:_smtpd /etc/mail/passwd
$ doas chmod 0440 /etc/mail/passwd
If you want to use DKIM to sign your outgoing mail it is time to create the key for it:
$ doas mkdir /etc/mail/dkim
$ doas openssl genrsa -out /etc/mail/dkim/example.net.key 1024
$ doas openssl rsa -in /etc/mail/dkim/example.net.key -pubout \
> -out /etc/mail/dkim/example.net.pub
$ doas chgrp _rspamd /etc/mail/dkim/example.net.key
$ doas chmod 0640 /etc/mail/dkim/example.net.key
There are some entries required in the DNS zone of your domain. The following command will generate a text file ready to import into the zone file:
$ echo ' MX 10 mail.example.net.' > rrs.txt
$ echo ' TXT "v=spf1 mx -all"' >> rrs.txt
$ echo 'mail A 192.0.2.10' >> rrs.txt
$ echo ' AAAA 2001:db8::c000:020a' >> rrs.txt
$ echo 'pop3 CNAME mail' >> rrs.txt
$ echo '$ORIGIN _domainkey.example.net.' >> rrs.txt
$ pubkey=$(sed /^-/d /etc/mail/dkim/example.net.pub | tr -d '\n')
$ echo "default TXT \"v=DKIM1;k=rsa;p=${pubkey}\"" >> rrs.txt
Of course you can use any certificate provider who supports the ACME protocol. I use Let’s Encrypt because they provide certificates for free, which is a huge win if you run a small site like this one.
You will use httpd(8) to answer the
challenges. Create a /etc/httpd.conf
similar to this one:
server "mail.example.net" {
listen on egress port http
alias "pop3.example.net"
root "/"
location "/.well-known/acme-challenge/*" {
request strip 2
root "/acme"
}
}
types {
include "/usr/share/misc/mime.types"
}
Test your configuration, enable and start httpd(8):
$ doas httpd -n
$ doas rcctl enable httpd
$ doas rcctl start httpd
Now acme-client(8) needs to know what to do and with whom. Take
/etc/examples/acme-client.conf
, adapt it to your needs and save the
result as /etc/acme-client.conf
:
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
authority letsencrypt-staging {
api url "https://acme-staging-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-staging-privkey.pem"
}
domain mail.example.net {
alternative names {
pop3.example.net
}
domain key "/etc/ssl/private/mail.example.net.key"
domain certificate "/etc/ssl/mail.example.net.crt"
domain full chain certificate "/etc/ssl/mail.example.net.fullchain.pem"
sign with letsencrypt
}
This config will issue a valid certificate right away. If you feel like
testing in the first place, you should change the line sign with
to
letsencrypt-staging
until you feel comfortable with the process.
Before you can get your certificate you must make sure pf(4) lets the requests actually pass through to httpd(8). Add a rule similar to the following one to your pf.conf(5):
pass in log on egress proto tcp from any to egress port http
After adding this rule to /etc/pf.conf
check the file and load it into
pf(4) with the following commands:
$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf
Now you can get your certificate using the following command:
$ doas acme-client mail.example.net
Certificates have an expiry date, like groceries. You may want to make
sure your certificate gets renewed automatically before it expires. The
file /etc/daily.local
can take care of this for you:
#!/bin/sh
/usr/sbin/acme-client mail.example.net
[[ $? -eq 0 ]] && rcctl restart smtpd dovecot
ClamAV will be used by rspamd to scan attachments for malware. To do so you need some configuration for ClamAV first in order to run it as a daemon and to keep the malware database up to date. Or if you run a malware scan server in your environment you can connect rspamd to it.
First you configure the daemon freshclam
to make sure the malware database of ClamAV stays up to date. The file
/etc/freshclam.conf
contains the following settings:
LogTime yes
LogSyslog yes
LogFacility LOG_DAEMON
DatabaseMirror db.ch.clamav.net
DatabaseMirror database.clamav.net
NotifyClamd /etc/clamd.conf
Enable and start freshclam now so it has time to update the signature database for ClamAV:
$ doas rcctl enable freshclam
$ doas rcctl start freshclam
Next you configure clamd.
In /etc/clamd.conf
the following lines are set:
LogTime yes
LogSyslog yes
LogFacility LOG_DAEMON
TemporaryDirectory /tmp
LocalSocket /var/clamav/clamd.sock
TCPSocket 3310
TCPAddr 127.0.0.1
User _clamav
DetectPUA yes
AlertEncrypted yes
AlertEncryptedArchive yes
AlertEncryptedDoc yes
AlertOLE2Macros yes
AlertPhishingSSLMismatch yes
AlertPhishingCloak yes
MaxRecursion 12
You may want to study the man page of clamd.conf and consider each of the options named Alert*. Some of these may block attachments you actually don’t want to get blocked on your mail server. As soon as you are happy with your configuration it is time to enable and start clamd:
$ doas rcctl enable clamd
$ doas rcctl start clamd
You may get a timeout warning after the start command. Using pgrep(1) you can check if clamd is actually running or not. In most cases it will be running and you can ignore the timeout message.
The preferred storage for rspamd data is Redis. It
used to be one of those packages you could just install and start. But
not anymore. First of all you will want Redis to listen to a UNIX socket
on the local machine. Those come with far less overhead than TCP sockets
and therefore speed up the communication between rspamd and Redis. Add
the following two lines to /etc/redis/redis.conf
:
unixsocket /var/run/redis/redis.sock
unixsocketperm 770
Make sure that rspamd can write to the socket. Unfortunately Redis does
not support setting owner/group for the socket. So you have to make the
user _rspamd
a member of the group _redis
:
$ doas usermod -G _redis _rspamd
If the logs of rspamd show messages containing:
ERR max number of clients reached
it is necessary to increase the number of allowed clients in Redis. By
default this is set to 96. You can increase the value by tweaking the
setting maxclients
in /etc/redis/redis.conf
:
maxclients 128
You might need to use even higher number, depending on what modules of rspamd you actually use with Redis.
rspamd will cover all the extra needs we have today when running a mail server out in the wild. It provides a highly customizable and trainable spam filter, malware filter, greylisting and DKIM signing.
While most modules work out of the box the DKIM signing needs
configuration in order to find the key it should use to sign mails.
Create the /etc/rspamd/local.d/dkim_signing.conf
containing this:
allow_username_mismatch = true;
sign_networks = ["192.0.2.11", "[2001:db8::c000:020b]"];
domain {
example.net {
path = "/etc/mail/dkim/example.net.key";
selector = "default";
}
}
The line sign_networks
is only needed if you have other servers in the
same domain that will use this MX as relay.
Furthermore you have to tell rspamd under which circumstances it should
perform DKIM signing only. The common cases for this are mails from
authenticated users and mails from other systems in the same domain. In
/etc/rspamd/local.d/settings.conf
add the following two blocks:
sign_auth {
id = "sign_auth";
authenticated = true;
apply {
symbols_enabled = ["DKIM_SIGNED"];
flags = ["skip_process"];
}
}
sign_only {
id = "sign_only";
ip = ["192.0.2.11", "[2001:db8::c000:020b]"];
apply {
symbols_enabled = ["DKIM_SIGNED"];
flags = ["skip_process"];
}
}
In case you use ClamAV for malware scanning this module of rspamd needs
some configuration too. Create the file
/etc/rspamd/local.d/antivirus.conf
with the following content:
clamav {
action = "reject";
message = '${SCANNER}: virus found: "${VIRUS}"';
scan_mime_parts = true;
scan_image_mime = false;
symbol = "CLAM_VIRUS";
type = "clamav";
prefix = "rs_cl_";
servers = "/var/clamav/clamd.sock";
whitelist = "${DBDIR}/wl_antivirus.map.local";
}
These settings are very strict by rejecting every mail that scans positive for malware. Depending on your needs you may want to reconsider this and add a high score to the mail instead. This gives you the chance to put into quarantine instead of blocking it completely.
If rspamd keeps misclassifying mails from particular domains you may
want to improve the score of those mails by whitelisting these. Create
the file /etc/rspamd/local.d/multimap.conf
:
WHITELIST_SENDER_DOMAIN {
type = "from";
filter = "email:domain";
map = "${DBDIR}/wl_sender_domain.map.local";
score = -5.0;
}
Some of the modules of rspamd work best using Redis as storage. To make
sure all these modules use your local Redis instance create a file
/etc/rspamd/local.d/redis.conf
containing the following line:
servers = "/var/run/redis/redis.sock";
In case you have not enabled the UNIX domain socket for Redis you can
replace the path by localhost
so it will use the TCP connection
instead.
In case you want to redirect the logging of rspamd from
/var/log/rspamd/rspamd.log
to the general /var/log/maillog
you need
to create the file /etc/rspamd/local.d/logging.inc
with the following
content:
type = "syslog";
facility = "mail";
level = "notice";
It is time to enable and start rspamd and its memory storage Redis:
$ doas rcctl enable redis rspamd
$ doas rcctl start redis rspamd
First, follow the pkg-readme of Dovecot and create an own login class
for it in /etc/login.conf
:
dovecot:\
:openfiles-cur=1024:\
:openfiles-max=2048:\
:tc=daemon:
I recommend to you to put the actual configuration of Dovecot into
/etc/dovecot/local.conf
and leave all the other config files alone
(with one exception further down). This way updates won’t destroy your
configuration. For a POP3-only configuration the file should look
similar to this one:
hostname = pop3.example.net
listen = 192.0.2.10, 2001:db8::c000:020a
login_greeting = "%s.example.net ready"
mail_home = /home/vmail/%d/%n
mail_location = mbox:~/mbox
pop3_fast_size_lookups = yes
pop3_no_flag_updates = yes
pop3_uidl_format = %g
protocols = lmtp pop3
ssl = yes
ssl_cert = </etc/ssl/mail.example.net.fullchain.pem
ssl_key = </etc/ssl/private/mail.example.net.key
ssl_dh = </etc/ssl/dh4096.pem
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ALL:!DH:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH
ssl_prefer_server_ciphers = yes
passdb {
driver = passwd-file
args = scheme=blf-crypt /etc/mail/passwd
}
service auth {
unix_listener auth-userdb {
mode = 0600
user = vmail
}
}
service lmtp {
user = vmail
}
service pop3-login {
inet_listener pop3 {
address = 127.0.0.1, ::1
}
}
service stats {
unix_listener stats-writer {
user = vmail
group = _dovecot
}
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/home/vmail/%d/%n
}
The configuration makes POP3 available on the public IP addresses using TCP port 995 which requires an encrypted connection right from the start. This makes it hard for users to send clear text passwords over an clear text connection. For debugging purposes there is a clear text connection available on localhost TCP port 110.
Due to a bug in the config parser of Dovecot you must comment out two
lines in /etc/dovecot/conf.d/10-ssl.conf
or the loading of the
certificate and key files will fail:
$ doas sed -i "/^ssl_[cert|key]/s/^/#/" /etc/dovecot/conf.d/10-ssl.conf
Check your configuration so far by testing the login for mail users with the following commands:
$ doas doveadm user muser@example.net
$ doas doveadm auth login muser@example.net
The first command should give you information about the account muser
while the second one should check if the password you’ve set for muser
is correct.
Beside POP3 Dovecot also listens for LMTP connections on the local UNIX
socket /var/dovecot/lmtp
. OpenSMTPD will use this socket to hand over
received mails to Dovecot.
Your server probably got another hostname, so make sure OpenSMTPD always identifies with the right name:
# echo "mail.example.net" > /etc/mail/mailname
Next, you want to make sure that OpenSMTPD knows about the valid
recipient addresses on the system and which of the default addresses get
redirected to whom. You define such a table(5)
in /etc/mail/virtuals
:
abuse@example.net: muser@example.net
hostmaster@example.net: muser@example.net
postmaster@example.net: muser@example.net
muser@example.net: vmail
Each valid mailbox on the left side either gets redirected to another valid mailbox defined in this file or to the system user that handles mails for us. Every recipient address that points to the system user will get it’s own mailbox.
There is another table called trusted
. You can put IP addresses in it
of hosts that you trust although both DNS checks fail for these. For the
below example configuration to work you need to create the file at
least:
$ doas touch /etc/mail/trusted
If you want to redirect local mails into one of the mailboxes of the
domain you should add a line similar to the following one to
/etc/mail/aliases
:
user: muser@example.net
Up next is the actual configuration of OpenSMTPD. I suggest you start
out with a fresh /etc/mail/smtpd.conf
- either put away the original
or clear it first.
name="mail.example.net"
table aliases file:/etc/mail/aliases
table passwd passwd:/etc/mail/passwd
table trusted file:/etc/mail/trusted
table virtuals file:/etc/mail/virtuals
smtp ciphers "TLSv1.3:TLSv1.2:!NULL"
smtp max-message-size "10M"
pki $name cert "/etc/ssl/mail.example.net.fullchain.pem"
pki $name key "/etc/ssl/private/mail.example.net.key"
filter trusted phase connect match src <trusted> bypass
filter no_rdns phase connect match !rdns disconnect \
"550 rDNS is required around here"
filter no_fcrdns phase connect match !fcrdns disconnect \
"550 FCrDNS is required around here"
filter rspamd proc-exec "filter-rspamd"
filter checks chain { trusted, no_rdns, no_fcrdns, rspamd }
listen on lo0
listen on $name tls pki $name filter checks
listen on $name smtps pki $name filter checks
listen on $name port submission tls-require pki $name \
auth <passwd> filter rspamd
action "local" mbox alias <aliases>
action "deliver" lmtp "/var/dovecot/lmtp" rcpt-to virtual <virtuals>
action "outbound" relay
match from local for local action "local"
match from any for domain "example.net" action "deliver"
match from local for any action "outbound"
match auth from any for any action "outbound"
Check your configuration and restart smtpd(8):
$ doas smtpd -n
$ doas rcctl restart smtpd
So far pf(4) is blocking access to the ports of the daemons you have
configured and started. If you are confident that your setup is OK it is
time to go live with the services. Add the following rules to
/etc/pf.conf
:
mail="{ smtp smtps submission pop3s }"
table <bruteforce> persist file "/etc/pf.bruteforce"
block drop in log quick on egress from <bruteforce> to any
pass in log on egress proto tcp from any to egress port $mail \
(max-src-conn 5, max-src-conn-rate 1/1, overload <bruteforce> flush)
These rules allow traffic to pass to the TCP ports you have configured for OpenSMTPD and Dovecot. Packets from misbehaving clients get dropped silently. Misbehaving is defined as opening more than five connections from the same source IP or opening connections faster that one per second.
After adding these rules to /etc/pf.conf
check the file and load it
into pf(4) with the following commands:
$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf
If you open one or more ports that require authentication - like 587/tcp (submission) above - you will face brute force attacks sooner or later. Limiting the allowed connections and rates in the pf(4) rule does block common brute force attackers effectively.
But there is this other kind of attackers. Those who try to fly under the radar by only connecting once every minute or so. Although this does not match the definition of a brute force attack, these connections tend to fill the logs. And by guessing common combinations of user name and password they might actually land a lucky punch.
One way to deal with the problem could be to extend the script addbrute.sh with the following lines:
# Catch authentication failures from OpenSMTPD
for id in $(grep failed-command.*AUTH ${logf}) ; do
grep ${id}.*address= ${logf} | sed "s/.*address=//;s/ .*//"
done >> ${dump}
# Catch shady connections to POP3
grep pop3-login.*no auth attempts" ${logf} | sed "s/.*rip=//;s/, .*//" >> ${dump}
Beware that this script must be run as root and that it might affect your legitimate users as well as any attackers. The script doesn’t have any restrictions about the age of the log entries. This makes it hard to remove false positives from the table without having them readded during the next run of the script.
No matter how good a spam detection system is, you will have both false
positives (messages that are actually not spam) and false negatives
(spam messages that are not detected as such). Lucky for you rspamd
comes with a web interface to monitor and tune it. By default this web
interface is accessible without any kind of authentication. Therefore it
is only listening on localhost. The easiest way to access it is using
port forwarding of ssh(1). Adding a line
like this to ~/.ssh/config
should do the trick:
Host mail.example.net
LocalForward 11334 127.0.0.1:11334
Whenever you are logged in to mail.example.net using ssh(1) you can access the rspamd web interface in your browser with this link: http://localhost:11334/
Adding an additional mailbox to the server is as easy as using the following three commands:
# echo "nuser@example.net:$(smtpctl encrypt Password)::::::" >> /etc/mail/passwd
# echo "nuser@example.net: vmail" >> /etc/mail/virtuals
# smtpctl update table virtuals
Dovecot will take care of creating the required files and folders to store the mails as soon as the first mail arrives to the new mailbox.
Disabling a no longer needed mailbox is equally simple:
# sed -i /nuser@example\.net/d /etc/mail/passwd
# sed -i /nuser@example\.net/d /etc/mail/virtuals
# smtpctl update table virtuals
If you want to remove the mailbox completely including any mails left in it you can issue this command after disabling the mailbox:
# rm -rf /home/vmail/example.net/nuser