Warning: This is a stream of consciousness describing my latest adventure installing a mail server. It is mostly a reminder for myself but I am publishing it here, so it might be helpful for others as well. No I am not the owner of example.com.
As I am about to decentralize all the thing I am going to run my own mail server for family members. As an initial host I have chosen the cheapest DigitalOcean VM I could rent which should be more than sufficient to host the mail. I have followed this tutorial to install OpenBSD underhanded.
There are quite a few caveats to that:
- No copy / paste in the web console, this means you will retype IPs, netmasks and so on from the website...
- Don't use the DigitalOcean name servers, the don't forward requests external IPs.
- The web console is dysfunctional when it comes to keyboard encodings != us. This means no way to type a slash "/" during the OpenBSD installer, especially in the disklabel editor (you cannot escape to a shell there to switch using kbd(1)). Try entering a v6 address without a ":". The trick is to escape to a shell and try different layouts "de" / "us" for different stages of the installer.
- Entering secure passwords as generated by pee is tedious as you don't know which of the funky chars you are actually typing. As a workaround to that I have written a quick hack in CHICKEN scheme to generate passphrases from the debian ngerman wordlist:
(import foreign) (use (srfi 1 4 13)) (define (random-bytes len) (cond-expand (openbsd: (arc4random len)) (linux: (with-input-from-file "/dev/urandom" (lambda () (read-u8vector len)))) (else (with-input-from-file "/dev/random" (lambda () (read-u8vector len)))))) #+openbsd (define (arc4random len) (let ((buf (make-blob len))) ((foreign-lambda void "arc4random_buf" (scheme-pointer void) size_t) buf len) (blob->u8vector buf))) (define wordlist-file "/usr/share/dict/ngerman") (define words (with-input-from-file wordlist-file read-lines)) (define number-of-words (length words)) (define entropy-per-word (inexact->exact (floor (* (/ (log number-of-words) (log 2)) 100)))) (define (main entropy) (let ((needed-words (inexact->exact (ceiling (/ entropy entropy-per-word))))) (print "Loaded word list (" wordlist-file ") with " number-of-words " words.") (print "Generating a phrase of " needed-words " words with an entropy of " entropy-per-word " bits per word.") (let loop ((phrase '()) (n needed-words)) (let* ((r (u8vector->list (random-bytes 2))) (i (+ (arithmetic-shift (car r) 8) (cadr r)))) (if (zero? n) (print "Phrase: '"(string-join (reverse phrase)) "'") (loop (cons (list-ref words (modulo i number-of-words)) phrase) (sub1 n))))))) (main (string->number (cadr (argv))))
With this we get fine passphrases like "Dichterfürst Kassettentonbandgerät Jahreskongresse Hydraulikverbesserungen Bauskizzen Gräuelmärchen".
So after setting up the usual: user in wheel, doas.conf, ssh keys, disabling root logins, applying patches etc. The fun starts.
Set your hostname to something other than the domain name. I didn't which I've regretted dearly, as you might see later.
I have followed the tutorial on the OpenSMTPd FAQ almost verbatim. First I have installed opensmtpd-extras from packages, as it contains a passwd input filter which we will use in the opensmtpd config. Also dovecot as a way to get to the mails.
Things not mentioned in the tutorial: You want to use smtpctl encrypt to generate the passwords for submission authentication. Also don't forget to run newaliases.
Also I have thrown away all the dovecot conf files and replaced them with the output of dovecot -n. Who needs a conf.d directory with several layers of indirection? I don't for this simple setup.
Next try to connect to imap and smtpd. Second failure of mine was to configure the MUA to actually use the submission port instead of port 25. My second error is related to the hostname. Initially I had a hostname equal to the domain name, say 'example.com'. That's a bad idea as opensmtpd identifies "local" connections via the hostname. So what has happened to me: The second accept rule in the smtpd.conf file never matched since all connections are seen as local, even the ones from virtual users. So the lookup for the expansion went to the wrong file resulting in the very frustrating "invalid recipient" error message.
I have resolved this by renaming the host name to 'mailer.example.com'.
Next I wanted to be a good mail citizen so I added SPF and DKIM to my setup. So next install dkimproxy from packages. This expands the smtp.conf a bit as we are rerouting outgoing submissions to the dkim_out proxy, which will be resubmitted by the proxy for us to relay.
The dkim_out.conf looks like this:
listen 127.0.0.1:10027 relay 127.0.0.1:10028 domain example.com signature dkim(c=relaxed) signature domainkeys(c=nofws) keyfile /etc/mail/dkim/private.key selector selector1
Generate a key using openssl:
openssl genrsa -out private.key 1024 openssl rsa -in private.key -pubout -out public.key
So the listen address will get outgoing mails IN and spits them back out properly signed.
So after doing this I got the following final smtp.conf file:
# tables table aliases file:/etc/mail/aliases table virtual-users file:/etc/mail/virtuals table passwd passwd:/etc/mail/passwd # PKI pki mail.example.com certificate "/etc/ssl/certs/mail.example.com.crt" pki mail.example.com key "/etc/ssl/private/mail.example.com.key" # To accept external mail, replace with: listen on all listen on lo0 listen on lo0 port 10028 tag DKIM_OUT # passed through mails from dkim proxy listen on egress port 25 tls pki mail.example.com listen on egress port 587 tls-require pki mail.example.com auth <passwd> accept tagged DKIM_OUT for any relay #signed mails just may leave the system accept from local for local alias <aliases> deliver to lmtp "/var/dovecot/lmtp" rcpt-to accept from any for domain "example.com" virtual <virtual-users> deliver to lmtp "/var/dovecot/lmtp" rcpt-to accept from local for any relay via smtp://127.0.0.1:10027 # pass on to dkim proxy
So next step is to announce the DKIM public key in a DNS record. I have added also the spf and DMARC entries while at it. The zone file looks like this:
$ORIGIN example.com. $TTL 1800 example.com. IN SOA ns1.digitalocean.com. hostmaster.example.com. 1467900139 10800 3600 604800 1800 example.com. 1800 IN NS ns1.digitalocean.com. example.com. 1800 IN NS ns2.digitalocean.com. example.com. 1800 IN NS ns3.digitalocean.com. example.com. 1800 IN A 11.22.33.44 example.com. 1800 IN MX 10 mail.example.com. mail.example.com. 1800 IN A 11.22.33.44 example.com. 1800 IN AAAA c0fe:babe:::::8001 mail.example.com. 1800 IN AAAA c0fe:babe:::::8001 selector1._domainkey.example.com. 1800 IN TXT k=rsa; t=s; p=MIGfMA0GCSqGb3DQEBAQUAA4GNADCBiQKBgQC0OWEGz4nMVGPdEzk6snF9rPu7cqFrip82nwcM1NqhLQdsUPjWJQacZV4B8er3E7HnCQu2NIw0DU0SuX9azvHkdWPPjyERwrbawt2mwCl2Ffxu169SshS8UDP7F0/U4dFoH40hEPsKCx2UyjNjk1MxBAvkYfJQ8WMA6wge+xWHKQID12df example.com. 1800 IN TXT v=spf1 mx a -all _dmarc.example.com. 1800 IN TXT v=DMARC1; p=none
I have found it very convenient to check these by sending mails to auth-results@verifier.port25.com.
Now I wanted to add some insult to injury for spammers so I have added spamd to the mix. For now I am sticking with the default config. The pf rules for this are taken verbatim from the pf.conf example.
The nospamd whitelist however needs some work as for example googlemail does not try hard enough with the same IP to get white-listed so one needs to add all addresses from their SPF records to the whitelist:
The quick and dirty way for me now has been:
# host -t txt _spf.google.com _spf.google.com descriptive text "v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all" # host -t txt _spf.google.com > ips # host -t txt _netblocks.google.com >> ips # host -t txt _netblocks2.google.com >> ips # host -t txt _netblocks3.google.com >> ips # tr ' ' '\n' < ips > ips1 # grep ^ip ips1 | sed 's,ip?:,,g' > ips
I have repeated that for ebay, amazon , web.de and gmx.de as those provide rather large lists for possible senders.
Also I have added the spamd-setup cron job and the stats cronjob as mentioned in the OpenSMTPd tutorial.
The only downside for now is that despite my mails being seen as legit by http://mail-tester.com and said port25.com my mails to gmail addresses still land in their spam folder. Maybe my test messages have been too short. I have filed an issue with their technical staff to sort out the issue. I do hope that it can be resolved.
That's it for now, I hope this will help you and my future self when rediscovering on how to set it all up.