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:
root
2026-08-04 09:20:49 +02:00
parent 2b84a8a493
commit e09f7618f6
3 changed files with 177 additions and 7 deletions
+6
View File
@@ -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
View File
@@ -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 (1133211334), 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**:
+32
View File
@@ -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: