Files
my-vault/00_Inbox/Clippings/2023/04/Self hosted mail server – if then else.md
T

418 lines
18 KiB
Markdown
Raw Normal View History

2026-01-05 13:03:55 +08:00
---
page-title: "Self hosted mail server if ? then : else"
url: https://www.ifthenel.se/self-hosted-mail-server/
date: "2023-04-27 17:18:34"
---
I have had enough. Its bad enough that social media has become the scourge of the Internet, but that doesnt mean key services such as email should have to be stronghold(ed?) by Big Corp such as Google, Microsoft, and other massive vendors.
The Internet was built on a foundation of globally connected computers that chose to share information between eachother and all was well.
Now, pretty much \_EVERYTHING\_ on the web is behind some kind of walled garden and/or being harvested for user behaviour studies to increase revenue (through ads, mostly).
But Id like to see if I can do something about this, in as far as my capabilities goes; its time to “take back” (Ive actually never hosted my own email server before, so its more of a “move the responsibility of”) my emails.
This is a part guide, part resource, part tutorial on how I set my email up on a VPS service. You wont be getting every single step as a command or how to edit your DNS records for your control panel on your registrar, but you should at least get a fairly detailed explanation of all the steps that are involved.
## A word of thanks
Id like to thank all the people involved for their tireless efforts in providing a solid out-of-the-box selfhostable mail server. I am a huge fan of all the individuals that put their time into creating a set of tools such as docker-mailserver and make it \_next\_ to a no-brainer to get it up and running.
Lets get to the grind.
## Tech stack
I have chosen to use the following techs
- External VPS hosting It is fairly cheap, and email shouldnt require exorbitant amounts of resources, and you \_have to\_ be able to edit reverse DNS records
- Debian 10 No particular reason other than I like Debian
- [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver) It has proven to be a one stop shop for an easy (well) get up and running component stack of different software that can deal with email
## Look at your inventory before you move on
### IP blacklisting delisting
Make sure your chosen server host isnt on any kind of IP or email blacklist. This activity is a tough one with a big risk of being costly (as in $$$) since you
- probably dont know what IP you get (this post assumes 123.123.123.123)
- dont know if that IP is on any list yet
- may not know of any tools can help you figure out what lists you are on
I dont have an answer to all of this. You can use [MX Toolboxs blacklist lookup tool](https://mxtoolbox.com/blacklists.aspx), but there are probably a lot more blacklisting going on that you will want to try and find so you have an ounce more chance of not getting you emails flagged as spam from the getgo.
I got to know my IP was on the BARRACUDA blacklist and I made an attempt at sending in a delisting request through their online form, and I seem to have been released from their clutches.
Microsoft/Outlook/Office are picky as all get out as well, so make additional efforts to get your IP delisted from there as well, even if you suspect youre not on their list: [https://sender.office.com/](https://sender.office.com/)
### rDNS (reverse DNS)
Get a host that supports rDNS (reverse DNS) and set a reverse value to something that you own, so that when a lookup happens (be it forward or reverse), they get *the same* hostnames and IP. E.g.;
$ host my.host // returns: "my.host has address 123.123.123.123"
$ dig +noall +answer -x 123.123.123.123 // returns: "123.123.123.123-in-addr.arpa \[number\] IN PTR my.host"
You could also use [MX Toolbox reverse lookup tool](https://mxtoolbox.com/ReverseLookup.aspx) to test this by supplying your mail hosts IP address and have it look this up for you.
## Setting up docker-mailserver (and moar)
### docker-mailserver
**THIS GUIDE ONLY COVERS docker-mailserver v10.2 AND LATER!**
1. Install `docker` and `docker-compose` on your host
2. Clone the docker-mailserver git repo (this way you will get all necessary files to start off including one of my personal favourites; `docker-compose.yml`): `git clone https://github.com/docker-mailserver/docker-mailserver`
3. Read the docker environment and topics following in the official README [https://github.com/docker-mailserver/docker-mailserver#create-a-docker-compose-environment](https://github.com/docker-mailserver/docker-mailserver#create-a-docker-compose-environment)
4. Do as the README suggests and then start the mailserver
$ `docker-compose up`
5. Wait for the startup to fail with a message along the lines of *mailserver | \[ ERROR \] Shutting down..*
6. Shut down the server again
`$ docker-compose down`
**Here be dragons. Read this. Its important.** I have based a lot of this tutorial from [https://docker-mailserver.github.io/docker-mailserver/edge/examples/tutorials/basic-installation/](https://docker-mailserver.github.io/docker-mailserver/edge/examples/tutorials/basic-installation/) but this specific step we are coming to, creating keys for DKIM, is missing a crucial part; you have to create a mail account on your mail server before generating the DKIM. Why? No idea yet. Why isnt it mentioned in the doc? Beats me. But you should really really make it clear to the authors of this document that this is a serious flaw to not have included. I spent a lot of time on google to figure this out…..
1. Add an account to your mailserver:
`$ docker-compose run mailserver setup email add someone@example.com` and input a password when requested
2. Set up DKIM with your docker-mailserver tag of choice:
`$ docker-compose run mailserver setup config dkim`
When you have gotten this far without issues, you should be able to start the mailserver without the initial errors:
`$ docker-compose up -d`
You should now be able to interact with the server using the command pattern `docker-compose exec mailserver` from here on.
### DNS
This part is tricky, because DNS records are finicky, they need to be thoroughly tested and take HEAPS of time to update. You will spend quite some time with this if you dont get it right on the first try. Get yourself a couple of days vacation for this part.
#### DKIM
- TXT record
- Hostname: `mail._domainkey.my.domain`
- Value: Whatever is in the opendkim text file
Add the newly created DKIM details to your domains DNS records by first echoing them for easy copy and paste:
`$ cat config/opendkim/keys/my.domain/mail.txt`
This produces something you should add to your DNS records. Heres the kicker; The strings are long and deliberately cut off because the maintainers dont know if your DNS hoster supports strings that are longer than 255 characters in length. So, you will have either a good time entering these as you might know your DNS hosters limitations, or, more likely, a bad time and you have to figure out how to get these details stored properly. Theres some help though. MX Toolbox has a DKIM checker that you can utilise to verify you have your stuff set up right: https://mxtoolbox.com/dkim.aspx Good luck.
In the DNS editor, your hostname for this TXT record should be `mail._domainkey.my.domain` and the value should be the long ass string output from the cat command above.
After a long wait, check with the [MX Toolbox DKIM Lookup](https://mxtoolbox.com/dkim.aspx)
#### SPF
- TXT record
- Hostname: @
- Value: `v=spf1 mx ~all`
This is simpler, because there are very few parameters. Its still required for your emails to have a decent chance of not being marked as spam. There can be some benefit to adding refernces to *other* known SPF records, such as tthe ones used by Outlook, etc. One of those is “`spf.protection.outlook.com`
Add a TXT record to your `@` (hostname/zone/whatitscalled) with the value: `v=spf1 mx ~all`
Tip: If you want to add Microsoft Outlooks SPF, you would change the value to `v=spf1 include:spf.protection.outlook.com mx ~all`
#### Subdomain
- A record
- Hostname: mail
- Value: IP address of your mail server host
Also a simpler DNS record change. You want to have a subdomain that keeps tabs on the IP address of you mail server host.
1. Add an A record to the subdomain `mail` with the value of your mail server host IP address
#### MX
- MX record
- Hostname: `@` or `mail.my.domain.`
- Value: `mail.my.domain`
This is used for telling every server on the internets that you have a mail server somewhere with a certain subdomain name
1. Add an MX record to the `@` (hostname/zone/whatitscalled) with the value:
`mail.my.host.`
#### DMARC
- TXT record
- Hostname: \_dmarc.my.domain
- Value: `v=DMARC1; p=quarantine; rua=mailto:postmaster@my.domain; ruf=mailto:postmaster@my.domain; fo=1; adkim=s; aspf=s; pct=100; rf=afrf; ri=86400; sp=quarantine`
Complicateder piece of DNS record than the last two, because there are a bunch of parameters in the TXT records that you should read up on and understand. Ill just give you what I use for now.
1. Add a TXT record to the `_dmarc` hostname with the value: `v=DMARC1; p=quarantine; rua=mailto:postmaster@my.domain; ruf=mailto:postmaster@my.domain; fo=1; adkim=s; aspf=s; pct=100; rf=afrf; ri=86400; sp=quarantine`
### Testing
Heres the part where you need to verify and verify again that everthing is in working order. Mostly MX Toolbox to the rescue.
#### MX
- [https://mxtoolbox.com/MXLookup.aspx](https://mxtoolbox.com/MXLookup.aspx)
- Format: my.domain
What you are looking for in the results is something along the lines of
- Pref: 0
- Hostname: my.domain
- IP address: 123.123.123.123
If there are more than one result in the table, you want to ensure your email server, the one with the IP of 123.123.123.123, is at the top, with a “Pref” (priority) value that is lower than any of the others.
#### SPF
- [https://mxtoolbox.com/spf.aspx](https://mxtoolbox.com/spf.aspx)
- Format: my.domain
What you are looking for in the results is something along the lines of 4 rows, detailing the parameters of the SPF record. For instance, if the SPF record in DNS was set to: “v=spf1 include:spf.protection.outlook.com mx ~all” you want a result such as:
v
spf1
The SPF record version
+
include
spf.protection.outlook.com
Pass
The specified domain is searched for an allow.
+
mx
Pass
Match if IP is one of the MX hosts for given domain name.
~
all
SoftFail
Always matches. It goes at the end of your record.
#### DKIM
- https://mxtoolbox.com/dkim.aspx
- Format: my.domain:mail
What you are looking for in the results is something along the lines of 4 rows, detailing the parameters of the DKIM record. For instance, if the DKIM (mail.\_domainkey) was set to: “v=DKIM1; h=sha256; k=rsa; p=jasdfjidjelajdvlkjg” you want a result such as:
v
DKIM1
Version
Identifies the record retrieved as a DKIM record. It must be the first tag in the record.
h
sha256
Hash Algorithms
A colon-separated list of hash algorithms that might be used.
k
rsa (Length: 4096 bits)
Key Type
Key Type The type of the key used by tag (p).
p
jasdfjidjelajdvlkjg
Public Key
The syntax and semantics of this tag value before being encoded in base64 are defined by the (k) tag.
#### DMARC
- https://mxtoolbox.com/dmarc.aspx
- Format: my.domain
What you are looking for in the results is something along the lines of 10 rows, detailing the parameters of the DMARC record. For instance, if the DMARC (my.domain) was set to: “`v=DMARC1; p=quarantine; rua=mailto:postmaster@my.domain; ruf=mailto:postmaster@my.domain; fo=1; adkim=s; aspf=s; pct=100; rf=afrf; ri=86400; sp=quarantine`” you want a result such as:
v
DMARC1
Version
Identifies the record retrieved as a DMARC record. It must be the first tag in the list.
p quarantine Policy Policy to apply to email that fails the DMARC test. Valid values can be none, quarantine, or reject.
p
quarantine
Policy
Policy to apply to email that fails the DMARC test. Valid values can be none, quarantine, or reject.
rua
mailto:postmaster@my.domain
Receivers
resses to which aggregate feedback is to be sent. Comma separated plain-text list of DMARC URIs.
ruf
mailto:postmaster@my.domain
Forensic Receivers
Addresses to which message-specific failure information is to be reported. Comma separated plain-text list of DMARC URIs.
fo
1
Forensic Reporting
Provides requested options for generation of failure reports. Valid values are any combination of characters 01ds seperated by :.
adkim
s
Alignment Mode DKIM
Indicates whether strict or relaxed DKIM Identifier Alignment mode is required by the Domain Owner. Valid values can be r (relaxed) or s (strict mode).
adpf
s
Alignment Mode SPF
Indicates whether strict or relaxed SPF Identifier Alignment mode is required by the Domain Owner. Valid values can be r (relaxed) or s (strict mode).
pct
100
Percentage
Percentage of messages from the Domain Owners mail stream to which the DMARC policy is to be applied. Valid value is an integer between 0 to 100.
rf
afrf
Forensic Format
Format to be used for message-specific failure reports. Valid values are afrf and iodef.
ri
86400
Reporting Interval
Indicates a request to Receivers to generate aggregate reports separated by no more than the requested number of seconds. Valid value is a 32-bit unsigned integer.
sp
quarantine
Sub-domain Policy
Requested Mail Receiver policy for all subdomains. Valid values can be none, quarantine, or reject.
Theres also a test results table, where you want all the checkmarks to turn up green.
## TLS Cert using letsencrypt
My weapon of choice is and will probably for a long time be certbot/letsencrypt. I chose to use the officially provided script in “certonly” mode. It is fully possible to acquire a certificate without involing anything else. I had a small bump in the road where I included a domain that had an A record for my HTTP services and that failed the whole process because the HTTP service wouldnt supply the info needed for certbot to complete. The gist of the command, though, is:
$ sudo certbot certonly --standalone -d mail.my.domain1 -d mail.my.domain2 \[more parameters\]
and follow the instructions. Make sure to supply the *mail* domain, e.g. `mail.my.domain` and not only the root domain, as it can make things end up like it did for me in the beginning, where I dont have a webserver that can properly reply on the ports/URL:s that certbot expects.
Now, edit the `docker-compose.yml` file to add a volume that points to the letsencrypt cert store path. For my setup this would result in a line under the volumes-section:
...
- /etc/letsencrypt:/etc/letsencrypt:ro
...
Dont forget to restart your docker-mailserver, as a reload of details is required.
## Catchall for everything but valid email accounts
This took some digging around. docker-mailserver isnt really the authority here, but postfix, thats included in docker-mailserver. I did find a couple of nice pointers in the docker-mailserver issue tracker that gave me the following understanding to get catchall to work as I expect; Catching all email to a single catchall-specific mail account, and deliver mail designated to valid accounts without ending up in both catchall and the valid account inbox:
- Valid accounts need to have aliases to themselves
- The domain, e.g. my.domain, need to have an alias pointing to the catchall account
So, for the sake of completeness, this is how would go about it:
1. Create any valid email account(s) (these will require their own passwords)
2. Create the catchall account (this will also require its own password)
3. Create an alias to the catchall account with
`$ docker-compose exec mailserver setup alias add @my.domain @catchall.my.domain`
4. Edit the newly created file `config/postfix-virtual.cf` and modify it to the following format:
account1@my.domain account1@my.domain
account2@my.domain account2@my.domain
...
@my.domain catchall@my.domain
**The order is crucial.** You want the catchall to be at the bottom of this file at all times.
## Multiple domains, one server
This ties into the TLS Cert section, insofar that the command to generate valid certificates is:
$ sudo certbot certonly --standalone -d mail.my.domain1 -d mail.my.domain2 \[more parameters\]
and you will need to secure that you have your docker-compose.yml file set up to take into account the certificates:
...
\- /etc/letsencrypt:/etc/letsencrypt:ro
...
and you need to have restarted docker-mailserver to have it react to those changes.
And, you also need to have updated the `mailserver.env` file to tell docker-mailserver that youll be using letsencrypt.
Other than that, theres a few more things needed for your *second domain*. It is in general much the same as your primary domain, except for a few specific details that need to be unique to your second domain. Heres the complete list as to not miss out on anything
- MX DNS record
- SPF DNS record
- DKIM DNS record
- DMARC DNS record
- An email account tied to the second domain
Ok, thats a lot of stuff again! But heres the kicker; a few of them you can use as they mostly are from your primary domain, and others are super easy to set up.
The ones you can copy and easily modify from your primary domain are
- MX record You will use the same IP address for this as the primary one
- SPF record You use the same TXT value as the primary one
- DMARC record You use the same TXT value as the primary one, but I also modified the postmaster email address to align with the second domain
And this is the part where you need to do a little more work
### DKIM
This is very much like generating DKIM from the setup steps in this page;
1. Create an email account, e.g. `postmaster@second.domain`
2. Generate the DKIM DNS record details by running `$ docker-compose run mailserver setup config dkim` which will generate new keyfiles for your new domain (remember that the setup-script use the domain names of already created accounts to determine what domains to generate DKIM DNS records for)
3. Copy and paste the value from `$ cat config/opendkim/keys/second.domain/mail.txt` and create a new DNS record as explained in the DKIM-section in the setup steps on this page.
###### Changelog summary, 2021-10-14