summaryrefslogtreecommitdiff
path: root/memos/WM-068.txt
blob: c6aa9b514db9480d7864b922185613396614026d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
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>