Document the ports needed when the host is behind a firewall
New section 2 in docs/installation.md, split into inbound and outbound, because the outbound half is the one that gets forgotten. Inbound is the easy part and matches the nftables ruleset exactly: 25, 465/587, 993/143, 80, 4190 and the SSH port. Noted that port 80 is not needed for mail and therefore tends to get closed again after the installation - two months later the certificate renewal fails quietly. DNS-01 is the way out if it cannot stay open. Outbound: 25 for sending, 443 and 80 for Rspamd maps, certificates, packages and the Go build, UDP 11335 for fuzzy hashes, UDP 123 for the clock. And DNS on UDP *and* TCP 53 - TCP matters because DNSSEC answers are large and get truncated, which produces intermittent resolution failures that are hard to place. The trap gets its own block: unbound here is a full resolver and asks the root and authoritative servers directly. Corporate firewalls routinely permit DNS to the internal resolver only. That does not break the setup outright, it breaks it silently - DNSBL operators refuse queries from shared resolvers, and a blocklist that answers nothing looks exactly like a clean sender. The forward-zone workaround is documented together with what it costs. Also documented: behind NAT the A record must name the DNAT address and the PTR the SNAT address. If they differ, receiving works while sending fails on PTR and SPF - a miserable diagnosis, because mail arrives fine. Every verification command in the section was run on the live server first. Two matching entries in operations.md troubleshooting, for "mail arrives but nothing goes out" and for blocklists going quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+139
-7
@@ -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 <stage>`.
|
||||
@@ -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**:
|
||||
|
||||
|
||||
@@ -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 <message>` 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:
|
||||
|
||||
Reference in New Issue
Block a user