It can’t be DNS. There’s no way it could be DNS. It was DNS.
This article is not a full tutorial, just thoughts and notes on my OpenBSD email server assembly process. This article from poolp.org helped me greatly. I couldn’t be more thankful. Here is a wayback machine link in case the datacenter go boom.
And why not something else?
The Short: Because OpenBSD just works. Email isn’t hard. Linux (and even FreeBSD) are inclined overcomplicate it.
The Long: Before starting this project, I did a significant but intermittent amount of research into email servers and read many deprecated blog posts about the whole ordeal. There seemed to be a general superstition about how difficult email can be. The paranoia lurks, psyching out the hobbyist admin. No wonder with all the hearsay about rejection, DNS, reputation, and hacking.
It’s been nearly a year since I began thinking about hosting my own email. Obviously I adopted some of this superstition myself, or at least allowed it to enable my procrastination. Since this is a holiday weekend, I thought “What better to do than spend three days troubleshooting email?”
Initially I wanted to host the email server on FreeBSD but eventually landed on OpenBSD. Why? Because I wanted less headache. UNIX is like a loaded gun pointed at your foot. It is very easy to hurt yourself. OpenBSD mitigates some of this by mounting the loaded gun in a fixed position. It will require intense gymnastics to shoot yourself in the foot. OpenBSD seemed like the appropriate choice to minimize foot shooting, especially if I am providing email accounts for other people. I cannot in good faith provide a dumpster fire for my friends and colleagues, especially if there are important documents in that dumpster. The only flaming dumpster I endorse is the multi-user shell server created specifically for multiple users to start fires in.
Fires aside, I was pleasantly surprised with OpenBSD. I held a preconceived notion that ‘sometimes OpenBSD doesn’t do what you want it to do because it was designed to mitigate flaming dumpsters and foot-shooting.’ But boy was I wrong. From a desktop perspective, OpenBSD can feel limiting . . . but, in server space, OpenBSD feels very pleasant to use.
The Software Stack
Stop! Mailinabox and Mailcow are black boxes and we want nothing to do with them!
A mail server consists of multiple moving parts. Some would say ‘many’ moving parts, but I disagree. If you’ve ever had to set up a reverse proxy for your django or rails app, you’ve touched more moving parts than a basic email server.
|Component||Program/file||What it does|
|MTA||OpenSMTPD||Sends and receives mail via SMTP|
|MDA||Dovecot||Provides IMAP/POP so that users can use the server via a client|
|Spam filter||Rspamd||Progressively learns what is and isn’t spam|
|Auth||UNIX Auth||Allows a user to connect using the same credentials they would use to log in to the system locally|
|Mailbox||~/Maildir||Stores user email|
|LMTP||UNIX Sockets||Transfers mail between the MDA and the MUA|
Typically, the mail process can look like this:
Bob's MUA (via IMAP/POP) -> Bob's MDA (via LMTP) -> Bob's MTA (via IMAP) -> Alice's MTA
Alice's MTA (via LMTP) -> Alice's MDA -> Alice's Mailbox
When Alice wants to check her mail, it will look something like this:
Alice's Mailbox <- Alice's MDA <- Alice's MUA (via IMAP/POP)
They hate what they do not understand.
Modern email is a hack on top of DNS. In addition to an A record and an MX record, we need three TXT records: DMARC, DKIM, and SPF.
|Type||What it’s for|
|DMARC||Instructs the sender what to do if unable to deliver|
|DKIM||Server’s pubkey, used for cryptographically verifying the sender|
|SPF||Tells other servers who is allowed to send mail from our server|
And some example records:
|TXT||selector._domainkey.mail||“v=DKIM1;k=rsa;p=a 128 bit RSA key ;”||120|
|TXT||““v=spf1 ip4:123.456.78.9 -all”||120|
My biggest problem with the software stack was not actually the software stack. It was a DNS issue. For some god forsaken reason, Vultr automaticaly creates a * CNAME record. This is a problem because, depending on which record is found first, subdomain.domain.tld resolves to domain.tld. This was causing localhost to resolve to an external IP address.
Localhost binding to an external address is a big problem. Why? because if localhost resolves to anything other than 127.0.0.1 a service might try to bind to 123.456.78.9:port instead of 127.0.0.1:port. Redis and rspamd were both trying to bind to an external address and failing to start.
After I deleted the CNAME wildcard record, everything started just fine.
The biggest issue for DNS was that I actually misconfigured it. My mail server is actually at mail.domain.tld, not domain.tld. I Set up all my TXT records for domain.tld so google was automatically rejecting my email . . . because there was not valid DMARC, DKIM, or SPF. After modifying all the records so that they match the subdomian, I was still experiencing DKIM issues. In order to fix this, I copied the DKIM entry for mail.domain.tld to domain.tld and the issue seems to have been resolved.