diff --git a/README.md b/README.md index 0311fea..01c0a64 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ These have to be in place first: a static A record pointing at the server, a PTR record for the same name, outbound port 25 unblocked. Without those three there is no point — details in [docs/installation.md](docs/installation.md). +If the host sits behind a firewall of its own, section 2 of that document lists +every port that has to be permitted — **inbound and outbound**. The outbound +half is the one that gets forgotten: port 25 for sending, and DNS on UDP *and* +TCP 53 out to the whole internet, because the local resolver does its own +recursion. + ```sh git clone https://git.fhi.mpg.de/mike/mailserver.git cd mailserver diff --git a/docs/installation.md b/docs/installation.md index df0ec77..7431371 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -40,7 +40,139 @@ nc -zv gmail-smtp-in.l.google.com 25 # must say "succeeded" --- -## 2. Installing +## 2. Ports, when the host sits behind a firewall + +The server brings its own packet filter (`nftables`, default policy `drop`) and +opens only what is listed below. If there is **another** firewall in front of +it — a perimeter firewall, a security group, a NAT router — the same ports have +to be permitted there as well, or the host firewall's rules are moot. + +Everything here is IPv4. This setup does not configure IPv6 (see the note at +the end of this section). + +### Inbound: internet → server + +| Port | Proto | Service | Needed | +|---|---|---|---| +| **25** | TCP | SMTP, mail from other servers | **always** — without it no mail arrives at all | +| **465** | TCP | Submission, implicit TLS | for your own users to send from outside | +| 587 | TCP | Submission, STARTTLS | alternative to 465; one of the two is enough | +| **993** | TCP | IMAPS | for mail clients to read mail | +| 143 | TCP | IMAP with STARTTLS | optional, 993 is the better default | +| **80** | TCP | ACME challenge for Let's Encrypt | at issue **and at every renewal**, roughly every 60 days | +| 4190 | TCP | ManageSieve | only if clients edit their own rules | +| *your SSH port* | TCP | administration | yes, unless there is a console | + +Port 80 is the one people forget: it is not needed for mail, so it gets closed +again after the installation — and about two months later the certificate +quietly fails to renew. If it cannot stay open permanently, switch certbot to +the DNS-01 challenge instead (`certbot --preferred-challenges dns`), which +needs no inbound port at all. + +Nothing else has to be reachable from outside. Rspamd (11332–11334), Redis +(6379) and unbound (53) deliberately listen on `127.0.0.1` only. + +### Outbound: server → internet + +This is the half that gets forgotten, and it is where a corporate firewall +usually hurts. + +| Port | Proto | Destination | What for | +|---|---|---|---| +| **25** | TCP | any mail server on the internet | **sending**. The single most commonly blocked port. | +| **53** | UDP **and** TCP | **arbitrary internet name servers** | see the warning below | +| 443 | TCP | `maps.rspamd.com`, `updates.rspamd.com`, Let's Encrypt, `deb.debian.org`, `proxy.golang.org` | spam rule updates, certificates, packages, building `mailctl` | +| 80 | TCP | `deb.debian.org`, `sa-update.surbl.org` | packages, one Rspamd map served over HTTP | +| 11335 | UDP | `fuzzy1.rspamd.com`, `fuzzy2.rspamd.com` | fuzzy hashes — recognises mail already seen elsewhere | +| 123 | UDP | NTP servers | the clock. A wrong clock breaks DKIM validation and TLS. | + +TCP 53 matters as much as UDP 53: DNSSEC answers are large and are frequently +truncated, at which point the resolver retries over TCP. A firewall that only +lets UDP 53 through causes intermittent, hard-to-place resolution failures. + +> **The trap: DNS must go out to the whole internet.** +> +> `unbound` is set up here as a **full resolver**. It asks the root servers and +> then the authoritative name servers of each domain directly — it does not +> forward to anything. That is deliberate, for two reasons: +> +> - The DNSBL operators (Spamhaus and the rest) **block queries arriving +> through shared resolvers**. Asking through the company resolver gets you +> error codes instead of answers, and the blocklists silently stop working. +> - DNSSEC validation is what makes DANE possible, and DANE is what secures +> outbound TLS. +> +> Many corporate firewalls permit DNS only to the internal resolver. If +> outbound 53 to the internet genuinely cannot be opened, the setup still runs, +> but you have to add a forwarder to +> `/etc/unbound/unbound.conf.d/mailserver.conf`: +> +> ``` +> forward-zone: +> name: "." +> forward-addr: 10.0.0.53 # the resolver you are allowed to use +> ``` +> +> Be clear about the price: the DNSBL checks become unreliable to useless, and +> DANE only keeps working if that resolver validates DNSSEC and you trust the +> path to it. Spam detection then rests on Rspamd and Bayes alone. + +### Behind NAT + +Two things have to line up, and they are easy to get wrong separately: + +- The **A record** must name the public address at which the server is + reachable from outside (the DNAT address). +- The **PTR record** must exist for the address the server **sends from** (the + SNAT address) and resolve to the same name. + +If inbound and outbound use different public addresses, receiving works while +sending fails on PTR and SPF checks — and the diagnosis is miserable, because +mail arrives perfectly well. Pin the outbound address to the same one, or leave +NAT out of the path for this host. + +`mailctl` cannot see any of this: it reads its own address from the route to +the outside, which behind NAT is the private one. That is what +`/etc/mailserver/server.conf` is for — enter the public address there. + +### Verifying it + +From outside, against the server: + +```sh +for p in 25 80 143 465 587 993 4190; do nc -zv -w5 mail.example.com $p; done +``` + +From the server, outwards: + +```sh +nc -zv -w5 gmail-smtp-in.l.google.com 25 # sending +dig +short @198.41.0.4 . NS # UDP 53 straight to a root server +dig +short +tcp @198.41.0.4 . NS # TCP 53 as well +dig +short debian.org A # full recursion through unbound +curl -sSI https://maps.rspamd.com/ | head -1 # 443 outbound +timedatectl show -p NTPSynchronized # must say yes +``` + +If the DNSBL lookups are the question, this is the direct test — it must return +addresses in `127.0.0.x`, not an error: + +```sh +dig +short 2.0.0.127.zen.spamhaus.org A +``` + +### IPv6 + +Not configured. `inet_protocols = ipv4` in Postfix, `do-ip6: no` in unbound. +That is harmless as long as **no AAAA record exists** for the server. Publish +an AAAA record without adapting the configuration and remote servers will try +IPv6, get nowhere, and delivery will stall until they fall back. Either leave +IPv6 out entirely, or do it properly: `inet_protocols = all`, unbound on IPv6, +nftables rules for it — plus a second PTR record for the IPv6 address. + +--- + +## 3. Installing ```sh git clone https://git.fhi.mpg.de/mike/mailserver.git @@ -77,7 +209,7 @@ leisure. --- -## 3. What the script does +## 4. What the script does Ten stages. Each can be repeated on its own: `sudo install/mailserver-install --only `. @@ -110,7 +242,7 @@ When an existing file is replaced, the old version is put in --- -## 4. When certbot fails +## 5. When certbot fails The most common stumbling block, and almost always one of two reasons: @@ -136,7 +268,7 @@ accept the certificate. For experiments only. --- -## 5. Afterwards +## 6. Afterwards At the end the script prints every DNS record still missing. The same list any time: @@ -176,7 +308,7 @@ DKIM says anything else, the TXT record is wrong or has not spread yet. --- -## 6. Checking later that everything still matches +## 7. Checking later that everything still matches ```sh install/verify-templates @@ -191,7 +323,7 @@ change behaviour. --- -## 7. By hand instead of by script +## 8. By hand instead of by script If you would rather do every step yourself: `install/mailserver-install` is deliberately written as a readable sequence, one function per stage. Working @@ -208,7 +340,7 @@ The placeholders in `config/` are: --- -## 8. Migrating an existing server +## 9. Migrating an existing server The route to take when the point is not to build anew but to **move**: diff --git a/docs/operations.md b/docs/operations.md index 6f64dd1..d4fccee 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -466,6 +466,38 @@ without a client: Only if that fails is the password to blame rather than the ban. +### Mail arrives but nothing goes out + +Two candidates, and they look nothing alike in the log. + +**Outbound port 25 is blocked.** Postfix keeps the mail in the queue and +retries; `mailctl queue` grows, the log shows `connect to ...[...]:25: +Connection timed out`. Test it directly: + + nc -zv -w5 gmail-smtp-in.l.google.com 25 + +If that hangs, the block is somewhere in front of the server, not in the +configuration. See [installation.md](installation.md), section 2. + +**fail2ban has banned the sender.** Then the mail never reaches the server in +the first place — see the previous heading. + +### Blocklists and DANE stop working + +Symptom: spam that used to be caught gets through, and +`rspamc symbols ` no longer shows any `RBL_*` symbol. + +Almost always DNS. `unbound` here is a full resolver and asks the internet +directly; if a firewall in front redirects or blocks that, the DNSBL operators +answer with errors instead of results — silently, because a blocklist that says +nothing looks exactly like a clean sender. + + dig +short 2.0.0.127.zen.spamhaus.org A # must give 127.0.0.x, not an error + dig +short +tcp @198.41.0.4 . NS # TCP 53 outbound as well + +The ports required and the workaround if outbound DNS cannot be opened are in +[installation.md](installation.md), section 2. + ### Further checks After changing the configuration, verify before reloading: