summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:17 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:17 +0200
commitc6fd7ea89b6e0a96eb1d81989de891b4768b1044 (patch)
tree12e869044c80c889f83b588210cc0ac500cd7a6a /docs
parentf4c6628c8e85513d9fd110ead95682368a15a0fd (diff)
downloadmeshbay-c6fd7ea89b6e0a96eb1d81989de891b4768b1044.tar.gz
feat: email verification for registration, email change, and invitations
Registration now creates a pending account and sends a 6-digit code via email; the account activates only after verification. Email changes on the profile page follow the same flow. Group invitations send a notification email to the invitee (without revealing their address to the inviter) containing the invite code and hub link. Backend: blind HMAC-SHA256 email index for uniqueness without decryption, mail.py for localhost Postfix delivery, verification endpoints, cleanup of expired codes and stale pending accounts, startup backfill of email_hash for existing users. Frontend: 3-phase register page, inline email change verification on profile, invite-notify call with status display. All 10 locales updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/MAIL-SERVER.md218
1 files changed, 218 insertions, 0 deletions
diff --git a/docs/MAIL-SERVER.md b/docs/MAIL-SERVER.md
new file mode 100644
index 0000000..c5a0d17
--- /dev/null
+++ b/docs/MAIL-SERVER.md
@@ -0,0 +1,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.