summaryrefslogtreecommitdiffstats
path: root/docs/MAIL-SERVER.md
blob: c5a0d17160a3410c9ed7814a4e45c019b9172b1a (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
# Mail Server — Postfix send-only with SPF/DKIM/DMARC

> Target: meshbay.org (Ubuntu 26.04 LTS, OVH VPS 164.132.246.44)
>
> 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. Current DNS state (before setup)

| Record | Value | Status |
|---|---|---|
| A | `164.132.246.44` | OK |
| MX | `1 mx1.mail.ovh.net` / `5 mx2` / `100 mx3` | OK — receiving stays OVH |
| SPF | `v=spf1 include:mx.ovh.com -all` | Must add VPS IP |
| DKIM | (none) | Must create |
| DMARC | (none) | Must create |
| PTR (rDNS) | `vps-7148538e.vps.ovh.net` | Must change to `meshbay.org` |

## 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)

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 the VPS IP alongside the existing OVH include:

```
meshbay.org.  TXT  "v=spf1 ip4:164.132.246.44 include:mx.ovh.com -all"
```

**Add DKIM** — after running the install script which generates the key:

```
meshbay._domainkey.meshbay.org.  TXT  "v=DKIM1; h=sha256; k=rsa; p=<PUBLIC_KEY_BASE64>"
```

The script prints the exact record content. 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 (automated by the install script)

| Component | Role |
|---|---|
| **Postfix** | MTA — sends mail directly to recipient MX servers |
| **OpenDKIM** | Signs outgoing mail with the domain's DKIM private key |

Postfix is configured as **send-only** (no listening on port 25 from outside).

---

## 3. Installation

Run the provided script on the server:

```bash
# From the local machine:
scp QE/deploy/setup-mailserver.sh cbesson@meshbay.org:/tmp/
ssh cbesson@meshbay.org 'sudo bash /tmp/setup-mailserver.sh'
```

Or directly on the server:

```bash
sudo bash setup-mailserver.sh
```

The script:
1. Installs Postfix + OpenDKIM
2. Configures Postfix as send-only (inet_interfaces = loopback-only)
3. Generates a 2048-bit RSA DKIM key with selector `meshbay`
4. Wires OpenDKIM into Postfix via milter
5. Prints the DKIM TXT record to add to DNS
6. Sends a test email (if an address is given as argument)

## 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

The install script auto-detects Fedora and uses `dnf` instead of `apt`.
OpenDKIM is in the `opendkim` package, same as Ubuntu. The main difference is
the Postfix service name (`postfix` on both) and that SELinux may need a policy
for the OpenDKIM socket. The script handles this with `setsebool -P
dkim_milter_enable on` if SELinux is enforcing, and falls back to a custom
module if the boolean does not exist.