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
|
# Mail Server — Postfix send-only with SPF/DKIM/DMARC
> Target: meshbay.org (Ubuntu 26.04 LTS, OVH VPS 164.132.246.44 /
> 2001:41d0:20a:900::2389)
>
> Purpose: send confirmation codes to validate user email addresses at
> registration. The hub sends mail; it does not receive it. OVH MX servers
> (`mx1.mail.ovh.net` etc.) continue handling inbound mail for the domain.
---
## 1. DNS state
| Record | Value |
|---|---|
| A / AAAA | `164.132.246.44` / `2001:41d0:20a:900::2389` |
| MX | `1 mx1.mail.ovh.net` / `5 mx2` / `100 mx3` — inbound mail and redirections stay at OVH |
| SPF | `v=spf1 ip4:164.132.246.44 ip6:2001:41d0:20a:900::2389 include:mx.ovh.com -all` |
| DKIM | `meshbay._domainkey` — RSA 2048, `h=sha256` |
| DMARC | `v=DMARC1; p=quarantine; sp=quarantine; adkim=s; aspf=s; pct=100; rua=mailto:postmaster@meshbay.org` |
| PTR (rDNS) | `meshbay.org.` for both the IPv4 and the IPv6 address |
DMARC is at the intermediate `quarantine` step; `reject` is the target (§7).
## 2. What must happen
### 2.1 Reverse DNS (PTR)
Set via the **OVH control panel** (not DNS zone):
1. OVH Manager → Bare Metal Cloud → VPS → IP
2. Click the gear icon next to `164.132.246.44` → Modify reverse DNS
3. Set to: `meshbay.org.` (trailing dot)
4. Same for `2001:41d0:20a:900::2389` — Postfix sends over IPv6 whenever the
receiver has an AAAA MX (Gmail does)
Most receiving MTAs reject or score down mail from an IP whose PTR does not
match the HELO/EHLO hostname.
### 2.2 DNS zone records (OVH DNS zone editor)
**Update SPF** — add both VPS addresses alongside the existing OVH include:
```
meshbay.org. TXT "v=spf1 ip4:164.132.246.44 ip6:2001:41d0:20a:900::2389 include:mx.ovh.com -all"
```
**Add DKIM** — once the key is generated (§3.3):
```
meshbay._domainkey.meshbay.org. TXT "v=DKIM1; h=sha256; k=rsa; p=<PUBLIC_KEY_BASE64>"
```
The `p=` value is in `/etc/opendkim/keys/meshbay.org/meshbay.txt`, written by
`opendkim-genkey`. The selector is `meshbay`.
**Add DMARC** — start with `none` policy (monitoring), tighten to `quarantine`
then `reject` after confirming deliverability:
```
_dmarc.meshbay.org. TXT "v=DMARC1; p=none; sp=none; adkim=s; aspf=s; rua=mailto:postmaster@meshbay.org"
```
Target (after validation):
```
_dmarc.meshbay.org. TXT "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:postmaster@meshbay.org"
```
### 2.3 Server-side
| Component | Role |
|---|---|
| **Postfix** | MTA — sends mail directly to recipient MX servers |
| **OpenDKIM** | Signs outgoing mail with the domain's DKIM private key |
| **systemd-resolved drop-in** | Lets the domain's MX resolve on a host named after the domain (§9.2) |
Postfix is configured as **send-only** (no listening on port 25 from outside).
---
## 3. Installation
### 3.1 Packages
```bash
# Ubuntu — answer "Internet Site", mail name meshbay.org
sudo apt install postfix opendkim opendkim-tools mailutils
# Fedora
sudo dnf install postfix opendkim opendkim-tools mailx
```
### 3.2 Postfix — `/etc/postfix/main.cf`
```
smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
smtp_tls_security_level = may
smtp_tls_CApath = /etc/ssl/certs
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_loglevel = 1
myhostname = meshbay.org
myorigin = meshbay.org
mydestination = localhost.localdomain, localhost
mynetworks = 127.0.0.0/8 [::1]/128
inet_interfaces = loopback-only
inet_protocols = all
relayhost =
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
mailbox_size_limit = 0
recipient_delimiter = +
milter_protocol = 6
milter_default_action = accept
smtpd_milters = local:opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters
compatibility_level = 3.9
```
`inet_interfaces = loopback-only` makes the server send-only; `meshbay.org` is
absent from `mydestination` so that mail to its OVH mailboxes and redirections
goes through the MX lookup (§9.1).
### 3.3 DKIM key
```bash
sudo mkdir -p /etc/opendkim/keys/meshbay.org
sudo opendkim-genkey -b 2048 -d meshbay.org -s meshbay -D /etc/opendkim/keys/meshbay.org/
sudo chown -R opendkim:opendkim /etc/opendkim
sudo chmod 600 /etc/opendkim/keys/meshbay.org/meshbay.private
```
### 3.4 OpenDKIM
`/etc/opendkim.conf`:
```
Syslog yes
SyslogSuccess yes
LogWhy yes
Canonicalization relaxed/simple
Mode s
SubDomains no
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
InternalHosts /etc/opendkim/TrustedHosts
OversignHeaders From
Socket local:/var/spool/postfix/opendkim/opendkim.sock
PidFile /run/opendkim/opendkim.pid
UMask 007
UserID opendkim
```
| File | Content |
|---|---|
| `/etc/opendkim/KeyTable` | `meshbay._domainkey.meshbay.org meshbay.org:meshbay:/etc/opendkim/keys/meshbay.org/meshbay.private` |
| `/etc/opendkim/SigningTable` | `*@meshbay.org meshbay._domainkey.meshbay.org` |
| `/etc/opendkim/TrustedHosts` | `127.0.0.1`, `::1`, `localhost`, `meshbay.org` — one per line |
The socket lives inside the Postfix spool so that Postfix reaches it:
```bash
sudo mkdir -p /var/spool/postfix/opendkim
sudo chown opendkim:postfix /var/spool/postfix/opendkim
sudo chmod 750 /var/spool/postfix/opendkim
sudo usermod -aG opendkim postfix
```
### 3.5 systemd-resolved
Install the drop-in described in §9.2, then
`sudo systemctl daemon-reload && sudo systemctl restart systemd-resolved`.
### 3.6 Start
```bash
sudo systemctl enable --now opendkim postfix
sudo systemctl restart opendkim postfix
dig +short MX meshbay.org # must list the OVH MX (§9.2)
```
Then add the DNS records of §2.2 and set the PTR (§2.1).
## 4. Verification
### 4.1 Local checks
```bash
# Postfix is running and only on loopback
sudo ss -tlnp | grep :25
# Expected: 127.0.0.1:25, [::1]:25 only
# OpenDKIM is running
sudo systemctl status opendkim
# Send a test
echo "MeshBay mail test" | mail -s "Test from meshbay.org" your@email.com
```
### 4.2 Check DKIM signing
```bash
# Examine the Postfix log for DKIM signing confirmation
sudo journalctl -u postfix@- --since "5 minutes ago" | grep -i dkim
```
### 4.3 External validation
After DNS propagation (up to 24h, usually 1-2h on OVH):
- **SPF:** `dig TXT meshbay.org` — must show `ip4:164.132.246.44`
- **DKIM:** `dig TXT meshbay._domainkey.meshbay.org` — must return the public key
- **DMARC:** `dig TXT _dmarc.meshbay.org` — must return the policy
- **Full test:** send an email to `check-auth@verifier.port25.com` — the auto-reply
shows SPF/DKIM/DMARC pass/fail for each
- **Alternative:** https://www.mail-tester.com — send to the address shown, get a
score out of 10
### 4.4 PTR check
```bash
dig -x 164.132.246.44 +short
# Expected: meshbay.org.
```
## 5. Integration with MeshBay hub
The hub sends email via `localhost:25` (Postfix). No authentication needed —
Postfix listens only on loopback. Python code uses `smtplib`:
```python
import smtplib
from email.message import EmailMessage
def send_confirmation(to: str, code: str) -> None:
msg = EmailMessage()
msg["From"] = "noreply@meshbay.org"
msg["To"] = to
msg["Subject"] = "MeshBay — Confirm your email"
msg.set_content(
f"Your confirmation code is: {code}\n\n"
"This code expires in 30 minutes.\n"
"If you did not create a MeshBay account, ignore this email.\n"
)
with smtplib.SMTP("localhost", 25) as s:
s.send_message(msg)
```
The `From` address must be `@meshbay.org` — it must match SPF and DKIM signing
domain, or the message will fail authentication at the receiver.
## 6. Security considerations
- **Postfix is send-only.** `inet_interfaces = loopback-only` means it does not
accept connections from outside. No inbound port 25 in UFW.
- **No relay.** `mynetworks` is loopback only. The server cannot be used as an
open relay.
- **Rate limiting.** Not configured at the Postfix level (low volume, confirmation
codes only). Rate limiting should be done at the application level — the hub
should enforce per-IP and per-account rate limits on the confirmation endpoint.
- **DKIM private key.** Stored at `/etc/opendkim/keys/meshbay.org/meshbay.private`,
owned by `opendkim:opendkim`, mode `0600`.
## 7. Maintenance
### Rotate DKIM key
If the key is compromised or as a routine rotation (yearly is common):
```bash
sudo opendkim-genkey -b 2048 -d meshbay.org -s meshbay-2025 -D /etc/opendkim/keys/meshbay.org/
# Update /etc/opendkim/KeyTable with the new selector
# Add the new DKIM DNS record (new selector)
# Keep the old record for 48h so in-flight mail still verifies
# Remove the old record
sudo systemctl restart opendkim postfix
```
### Tighten DMARC
**Why this matters.** `p=none` tells receiving servers: "if a mail fails SPF+DKIM,
deliver it anyway — just report it to me." It protects nobody. An attacker can
send a phishing email `From: noreply@meshbay.org` from their own server and it
will land in the recipient's inbox normally. `p=reject` tells receivers to
**refuse** such mail outright — a fake MeshBay confirmation email with a link to
a credential-harvesting site never reaches the user. This is the defense against
someone impersonating the domain to steal user passphrases.
The gradual rollout exists only to verify that legitimate mail is not
accidentally blocked before locking the policy down.
Once deliverability is confirmed (SPF pass, DKIM pass on test emails):
1. `p=none` → `p=quarantine` — wait 2 weeks, check `rua` reports
2. `p=quarantine` → `p=reject` — the final target
## 8. Fedora 44 notes
Packages come from `dnf` (§3.1); OpenDKIM is in the `opendkim` package and the
Postfix service is `postfix`, as on Ubuntu. When SELinux is enforcing, Postfix
needs permission to connect to the OpenDKIM socket:
`sudo setsebool -P dkim_milter_enable on`. If that boolean does not exist, a
local module granting `postfix_smtpd_t` `connectto` on `opendkim_t`
`unix_stream_socket` does the same.
## 9. Hostname equal to the mail domain
The server's hostname is `meshbay.org`, which is also the domain whose mailboxes
and redirections live at OVH. Two settings keep mail addressed to `@meshbay.org`
(for example `devel@meshbay.org`, an OVH redirection) going to OVH's MX instead
of being handled on the server.
### 9.1 `mydestination` does not list the domain
```
mydestination = localhost.localdomain, localhost
```
Postfix delivers locally every domain listed in `mydestination`, without an MX
lookup. With `meshbay.org` in the list — which is also what the default
`$myhostname` expands to — a message to `devel@meshbay.org` is refused with
`550 5.1.1 User unknown in local recipient table`, or bounced by `postfix/local`
into the local mailbox of the sender, and never reaches OVH.
Consequence: mail for `root` and `cbesson` (cron output, bounces) is qualified
with `myorigin = meshbay.org` and goes to OVH too. It is delivered only if the
address exists there as a mailbox or a redirection.
### 9.2 systemd-resolved does not answer for the hostname
```
# /etc/systemd/system/systemd-resolved.service.d/no-synthesize-hostname.conf
[Service]
Environment=SYSTEMD_RESOLVED_SYNTHESIZE_HOSTNAME=0
```
systemd-resolved answers queries for the local hostname itself: A/AAAA with the
machine's addresses, and an authoritative empty answer (`aa`, `NOERROR`, no
records) for every other type, MX included. Postfix then falls back to the A
record and connects to its own port 25, which is loopback-only:
`status=deferred (connect to meshbay.org[164.132.246.44]:25: Connection refused)`.
The drop-in turns the synthesis off, so the query goes to the upstream resolver
and returns OVH's MX. Hostname resolution for `sudo` and other local tools is
unaffected: it goes through the NSS `myhostname` module, not through resolved.
After `systemctl daemon-reload && systemctl restart systemd-resolved`:
```bash
dig +short MX meshbay.org # must list mx1/mx2/mx3.mail.ovh.net
echo test | mailx -s test devel@meshbay.org
sudo journalctl -u postfix@- --since "1 min ago" | grep status=
# Expected: relay=mx1.mail.ovh.net[...]:25, status=sent
```
|