diff options
| author | netop://ウィビ <paul@webb.page> | 2026-04-11 14:24:49 -0700 |
|---|---|---|
| committer | netop://ウィビ <paul@webb.page> | 2026-04-11 14:24:49 -0700 |
| commit | 8c34d810af95fae0ef846f54370a8c88bfab7123 (patch) | |
| tree | 436beaf30f7b2b3f15741dd54a37e313964d1f7d /memos/WM-068.txt | |
Diffstat (limited to 'memos/WM-068.txt')
| -rw-r--r-- | memos/WM-068.txt | 399 |
1 files changed, 399 insertions, 0 deletions
diff --git a/memos/WM-068.txt b/memos/WM-068.txt new file mode 100644 index 0000000..c6aa9b5 --- /dev/null +++ b/memos/WM-068.txt @@ -0,0 +1,399 @@ + + + + + + + +Document: WM-068 P. Webb +Category: Self Hosting 2025.04.30 + + Stalwart Email Setup + +Abstract + + This is even better than my MiaB setup + +Body + + Stalwart[1] is an all‑in‑one mail server built for JMAP[2], the + ultra‑modern email standard we **should** be using but the tech + industry is cowardly and hella slow to move. Anyhoo! I recently setup + a new mail server with Stalwart because updating my aging + Mail‑in‑a‑Box[3] instance was unappealing and I wanna use cool new + things like JMAP. Running a mail server is never simple but you're + not here for simplicity, you're here to experience the future and + benefit from my learnings. + + Let's jump in. + + 1. Prerequisites + + 1. super dope domain + 2. radical web host that supports sending email + 3. an email address you aren't worried taking offline for a bit + + For me, I'm using `pidge.email` for my domain, Linode[4] (you get + $100 credit if you use my link) for my web hosting, and I used my + `chronver.org` email for my first test. If you aren't switching + mail servers, you've got it easy, just ignore that part. + + Linode requires you to open a support ticket if you want to send + email. No one wants to be labeled a spammy playground and they're + no exception. Thankfully, I was approved the following morning. + + In their welcome email, they suggest enabling rDNS (reverse DNS) + to make deliverability easier. One of the mail server validators + we'll use later in this post will check for this. + + 2. Installation + + This part is actually pretty straightforward, just follow the + tutorial[5]. There is actually a lot you could change for any + reason: running on Windows or Docker, changing the backend + database to SQLite or FoundationDB, whatever floats your boat. + + After changing the password from the auto‑generated one, you need + to reboot the server for it to take effect. + + 3. Setup + + When I setup[6] servers, Caddy[7] is installed as part of that + process so you'll need to install that if you haven't already. A + major perk of it over nginx is the automated certs and minimal + configuration. Here's my `Caddyfile`: + + ``` + pidge.email { + root * /var/www/html + file_server + } + + mta-sts.pidge.email, mail.pidge.email { + reverse_proxy 127.0.0.1:8080 + } + ``` + + A few notes here; the root path for your domain will be + `/usr/share/caddy` by default (which'll show you Caddy's "it's + working" screen with some tips on what to do next). The `mta-sts` + subdomain is necessary for other mail servers to view you as legit + and the `mail` subdomain is where the dashboard is (this is also + what you'll input to your favorite mail app when you add accounts + to it). + + After running `service caddy reload` you won't need to add the + `8080` port to your domain to access the Stalwart interface + anymore, just use the domain. If you see an error, you'll need to + run `service caddy status` and work through it. + + Stalwart's docs have some tips on using[8] Caddy but here's where + it's lacking: it wants you to create cron jobs to copy the certs + from Caddy to a directory within its scope but you need to do a + few things first. + + Create the `certs` folder the cron job expects to exist: + + ```sh + mkdir -p /opt/stalwart-mail/cert + ``` + + MANUALLY copy Caddy's certificate to Stalwart and convert to PEM + format (make sure you update `mail.pidge.email` to your + own domain). + + ```sh + cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.pidge.email/mail.pidge.email.crt > /opt/stalwart-mail/cert/mail.pidge.email.pem + ``` + + Do the same for the private key to said certificate (again, making + the necessary updates to your domain). + + ```sh + cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.pidge.email/mail.pidge.email.key > /opt/stalwart-mail/cert/mail.pidge.email.priv.pem + ``` + + _Now_ we can setup those cron jobs! + + ```sh + # this command opens up the cron thingy + crontab -e + ``` + + Paste these lines in (don't make me remind you about your + domain again): + + ``` + # caddy crt > pem + 0 3 * * * cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.pidge.email/mail.pidge.email.crt > /opt/stalwart-mail/cert/mail.pidge.email.pem + + # caddy key > pem + 0 3 * * * cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.pidge.email/mail.pidge.email.key > /opt/stalwart-mail/cert/mail.pidge.email.priv.pem + ``` + + These cron jobs will run at 3am every day. + + 4. Annoyances & Gotchas + + Stalwart's sidebar is…confusing. Visually it's easy to get lost in + it because there's no distinction of depth when you activate + different sections, and trying to go back to another setting you + saw before is an exercise in frustration. + + Thankfully, there's a search box at the top of the page and you + can generally get to where you wanna go quicker by using + that instead. + + I thought the "Fallback Administrator" was a different user + account but it's the same user that you login as. + + Whenever the sidebar trips you up and you just want to get back to + the domain list, click the "Management" link at the bottom. If you + don't see it, click "Maintenance" and then you'll see the + "Management" link toggle into view. + + There are a handful of features that are only available for the + enterprise version, with no way to hide them. If I really wanted + to, I could just compile a new binary without these features but + this is a minor complaint. A dev's gotta eat! + + 5. Tips 'n Tricks + + Stalwart wants you to add these TXT records to your mail + server's DNS: + + - `"v=spf1 a ra=postmaster -all"` for `mail.your.domain` + - `"v=spf1 mx ra=postmaster -all" for `your.domain` + + According to MailHardener's SPF Validator[9], you want to do + `"v=spf1 mx ~all"` instead, for reasons[10]: + + > SPF 'hard' fail is no longer recommended. For domains using + > DMARC, SPF 'hard' fail does not offer any security benefits over + > softfail, but may cause deliverability issues, even with + > valid DKIM. + + For domains I use my mail server for, I also add a TXT record for + the `www` subdomain with `"v=spf1 mx ~all"`. Note that I don't + have an A record for `www` this just makes the validator happy and + doesn't cost me anything, so why not? + + Speaking of other domains, there's a CNAME record you'll want to + change from what Stalwart recommends. Using my domains as + examples, Stalwart wants `mta-sts.inc.sh` to be a CNAME pointing + to `mail.pidge.email` when it should actually be an A record + pointing to the IP address of `inc.sh`. Here's the relevant + portion of my `Caddyfile`: + + ``` + mta-sts.inc.sh, inc.sh { + root * /var/www/inc.sh + import common + } + ``` + + Inside of `/var/www/inc.sh` is a `.well-known` directory + containing a `mta-sts.txt` file. + + But what if your site is a bit more fancy than regular degular + HTML? Your `Caddyfile` needs a lil' bit more. Here's `webb.page`: + + ``` + mta-sts.webb.page, webb.page { + handle_path /.well-known/* { + root * /var/www/html/.well-known + file_server + } + + encode gzip + reverse_proxy localhost:6433 + root * /var/www/html + } + ``` + + My homepage is a fancy web app that handles it's own routing so + the previous example wouldn't work. For my homepage, I handle the + `.well-known` path **first** and _then_ let the web app handle the + rest of the routes. + + Your MTA‑STS policy should be reachable and that's just not + possible with a CNAME. Using a CNAME makes sense when the mail + server and domain name are one and the same. + + This MTA‑STS validator[11] recommends a `max_age` of 28 days and + the `mode` should be "enforce." By default, Stalwart's `max_age` + is considerably less and the `mode` is "testing." + + Here's the output when CURLing `mta-sts.inc.sh`: + + ```sh + curl -k https://mta-sts.inc.sh/.well-known/mta-sts.txt + + # max_age: 2419200 + # mode: enforce + # mx: mail.pidge.email + # version: STSv1 + ``` + + Changing Stalwart's defaults _should_ work in the UI but I found + better luck with modifying its config file at + `/opt/stalwart-mail/etc/config.toml`. When you're done, you should + run `service stalwart-mail restart` or reboot the server if the + changes aren't sticking. + + Speaking of which, might as well share my + config (sans `authentication`): + + ```toml + certificate.default.cert = "%{file:/opt/stalwart-mail/cert/mail.pidge.email.pem}%" + certificate.default.default = true + certificate.default.private-key = "%{file:/opt/stalwart-mail/cert/mail.pidge.email.priv.pem}%" + directory.internal.store = "rocksdb" + directory.internal.type = "internal" + queue.outbound.tls.starttls = "require" + report.analysis.addresses = ["dmarc@*", "abuse@*"] + report.analysis.forward = true + report.analysis.store = "30d" + report.tls.aggregate.contact-info = "'postmaster@pidge.email'" + report.tls.aggregate.from-name = "'TLS Report'" + report.tls.aggregate.max-size = 26214400 # 25 mb + report.tls.aggregate.org-name = "'Pidge'" + report.tls.aggregate.send = "daily" + report.tls.aggregate.sign = "['rsa']" + server.hostname = "mail.pidge.email" + server.listener.http.bind = "[::]:8080" + server.listener.http.protocol = "http" + server.listener.https.bind = "[::]:443" + server.listener.https.protocol = "http" + server.listener.https.tls.implicit = true + server.listener.imap.bind = "[::]:143" + server.listener.imap.protocol = "imap" + server.listener.imaptls.bind = "[::]:993" + server.listener.imaptls.protocol = "imap" + server.listener.imaptls.tls.implicit = true + server.listener.pop3.bind = "[::]:110" + server.listener.pop3.protocol = "pop3" + server.listener.pop3s.bind = "[::]:995" + server.listener.pop3s.protocol = "pop3" + server.listener.pop3s.tls.implicit = true + server.listener.sieve.bind = "[::]:4190" + server.listener.sieve.protocol = "managesieve" + server.listener.smtp.bind = "[::]:25" + server.listener.smtp.protocol = "smtp" + server.listener.submission.bind = "[::]:587" + server.listener.submission.protocol = "smtp" + server.listener.submissions.bind = "[::]:465" + server.listener.submissions.protocol = "smtp" + server.listener.submissions.tls.implicit = true + server.max-connections = 8192 + server.socket.backlog = 1024 + server.socket.nodelay = true + server.socket.reuse-addr = true + server.socket.reuse-port = true + server.tls.certificate = "default" + session.mta-sts.max-age = "28d" + session.mta-sts.mode = "enforce" + storage.blob = "rocksdb" + storage.data = "rocksdb" + storage.directory = "internal" + storage.fts = "rocksdb" + storage.lookup = "rocksdb" + store.rocksdb.compression = "lz4" + store.rocksdb.path = "/opt/stalwart-mail/data" + store.rocksdb.type = "rocksdb" + tracer.log.ansi = false + tracer.log.enable = true + tracer.log.level = "info" + tracer.log.lossy = false + tracer.log.multiline = false + tracer.log.path = "/opt/stalwart-mail/logs" + tracer.log.prefix = "stalwart.log" + tracer.log.rotate = "daily" + tracer.log.type = "log" + webadmin.auto-update = true + ``` + + Unfortunately, if you want your TOML to have headers/sections, + they'll get inlined automatically when Stalwart reloads. Alas. + + 6. Bonus Points + + If you check out the `report.analysis.addresses` part of the + config, you'll see two email addresses you should setup for your + mail server: `dmarc@your.domain` and `abuse@your.domain`. You + should also setup `postmaster@your.domain`, and do the same for + the rest of the domains you connect to your mail server. The final + boss[12] and _final_ final boss[13] of mail validators are tough. + + Run your domains through these[14] other[15] validators[16] too. + The last one wants DNSSEC on your domains, which is a one‑click + setup on Cloudflare. Thank goodness! In my previous life[17] as a + bright‑eyed domain registry operator, I dealt with DNSSEC a lot. + It's a pain but more than wonderful when someone else makes it + easy to be secure. + + The neat thing about these `postmaster@your.domain` accounts is + getting aggregate TLS and DMARC reports from your usual suspects + (Amazon, Google, &c) but also from randos like me running their + own mail servers (hello `aperture-labs.org`)! + + Should this post reach Hacker News, ignore the grumps who last + hosted an email server when Windows XP was the new hotness or + think hosting your own email is a fruitless endeavor. I ran my + previous email server for _checks notes_ 10 YEARS?! Holy moly, + where did the time go…this server upgrade needed to happen, haha! + + FIN? + + I hope this wasn't _too_ rambly. When I self‑host things for the + first time I keep an unsaved text file open for days and use it as + a scratch pad of sorts, to make sense of later. This post is the + result of that process. + + One of my long‑standing backburner projects is a hosted email + service called, "Pidge," (from one of my favorite Pokémon + evolutions: Pidgey, Pigeotto, Pidgeot) and I registered + `pidge.email` seven years ago. Might as well use it now! + + Oh yeah, one last note; I use MailMate[18] and when adding new + accounts, you'll need to manually set "Mailbox Type" of your Trash + folder to "Deleted Messages," otherwise when you delete things a + new "Deleted Messages" folder will get created and that + looks dumb. + + Stalwart also gives you "Shared Folders"[19] which is a neat + feature of groups. Obviously this feature is suited + for work/enterprise. + + Much thanks to Maurus[20] for spending years working on Stalwart, + it's pretty sweet. I'll most likely be using this for another + 10 years. + + EDIT: Knew I'd remember something else as soon as I pushed this to + my server! When creating new accounts in Stalwart, you should make + "Login name" and "Email" be the same, it'll make things less confusing. + + 🕸️ + +References + + [1] <https://stalw.art> + [2] <https://jmap.io> + [3] <https://mailinabox.email> + [4] <https://www.linode.com/lp/refer/?r=51586d44e8e552b02d211b9675cab9e4132ce836> + [5] <https://stalw.art/docs/install/linux> + [6] <https://script.webb.page/server.sh> + [7] <https://caddyserver.com> + [8] <https://stalw.art/docs/server/reverse-proxy/caddy> + [9] <https://www.mailhardener.com/tools/spf-validator> + [10] <https://www.mailhardener.com/blog/why-mailhardener-recommends-spf-softfail-over-fail> + [11] <https://www.uriports.com/tools/mtasts-validator> + [12] <https://www.mail-tester.com> + [13] <https://email-security-scans.org> + [14] <https://www.mailhardener.com/tools/mta-sts-validator> + [15] <https://easydmarc.com/tools/mta-sts-check> + [16] <https://esmtp.email/tools/mta-sts> + [17] <https://blog.neuenet.com/post/vision> + [18] <https://freron.com> + [19] <https://stalw.art/docs/auth/principals/group> + [20] <https://github.com/mdecimus> |
