Zimbra Open Source Iptables: 7 Easy Rules and Mistakes
A firewall for Zimbra Open Source looks simple: allow the mail and web ports, drop the rest. The old version of this post did exactly that with seven iptables lines, and those lines have problems that only show up when you test them: replies to the server's own connections are dropped, the connection limit never fires, and several mail ports are missing. This rewrite tests the rules on a throwaway pair of containers and ends with a ruleset you can adapt.
Lab note: I did not install Zimbra Open Source. The "mail server" is a debian:13 container with iptables 1.8.11 (nf_tables backend) and socat listeners on ports 25, 443 and 7071; a second container acts as the client. The firewall behaviour is real, but the services behind the ports are stand-ins. The port list comes from the Zimbra wiki page "Ports" (KB 1391, verified for ZCS 8.8, read on 2026-11-05).
Contents
Which ports Zimbra Open Source needs open
The Zimbra wiki splits the ports of Zimbra Open Source into external ones for mail clients and internal ones used by Zimbra itself. The external list, for a Zimbra Open Source server that serves users directly:
| Port | Service |
|---|---|
| 25 | SMTP, incoming mail to Postfix |
| 80, 443 | HTTP and HTTPS web client |
| 110, 995 | POP3 and POP3 over TLS |
| 143, 993 | IMAP and IMAP over TLS |
| 465, 587 | SMTPS and mail submission |
The admin console on 7071 is listed under internal access, and the page advises being "as restrictive as possible". Ports 389 (LDAP), 7025 (LMTP) and 7026 (milter) are also internal. The old post opened only 25, 80, 110, 143 and 443, so a mail client on 587 or 993 could not connect. The result below is from the old rules, run against the stand-in listeners:
$ for p in 25 443 587 993 7071; do printf "port $p: "; nc -zv -w2 10.77.0.10 $p 2>&1 | tail -1; done
port 25: Connection to 10.77.0.10 25 port [tcp/smtp] succeeded!
port 443: Connection to 10.77.0.10 443 port [tcp/https] succeeded!
port 587: nc: connect to 10.77.0.10 port 587 (tcp) timed out: Operation now in progress
port 993: nc: connect to 10.77.0.10 port 993 (tcp) timed out: Operation now in progress
port 7071: nc: connect to 10.77.0.10 port 7071 (tcp) timed out: Operation now in progress
A "timed out" result means the packet was dropped silently, which is what -P INPUT DROP does. Clients wait for a long time before giving up, so a missing port looks like a slow server rather than a firewall problem. Also note that port 22 was never allowed in the old list; if you apply a default DROP policy over SSH, the session stops answering in the same way.
Zimbra Open Source firewall mistake 1: no rule for replies
With the old rules loaded (-P INPUT DROP, accepts for the five ports and loopback), I ran a connection from the server to the client. A mail server does this all the time: DNS lookups, outgoing SMTP, certificate checks. It failed:
$ time (nc -w3 10.77.0.20 9000 </dev/null; echo "exit=$?")
exit=1
real 0m3.006s
The server's SYN left (the OUTPUT policy is ACCEPT), the client answered, and the answer was dropped by INPUT because no rule accepts packets that belong to an existing connection. The fix is one line, placed near the top of the chain:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
After adding it, the same command printed the client banner at once, with exit=0 in 0.007 seconds. The conntrack match is documented in the iptables manual page.
Mistake 2: the connlimit rule comes after the ACCEPT
The old post limited connections per IP with a connlimit REJECT rule appended after the plain ACCEPT for port 25. Rules are read from the top and the first match wins, so the limit was never reached. I opened 14 parallel connections from one client, each held for 3 seconds:
$ { for i in $(seq 1 14); do ( (sleep 3) | nc -w2 10.77.0.10 25 2>&1 | head -1 ) & done; wait; } | sort | uniq -c
14 port 25
$ iptables -L INPUT -v -n --line-numbers | grep -E "dpt:25"
1 75 4008 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25
7 0 0 REJECT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25 #conn src/32 > 10 reject-with icmp-port-unreachable
All 14 connections were accepted, and the REJECT rule shows 0 packets. Put the limit before the accept and the result changes. With the fixed order for Zimbra Open Source, out of 14 connections, 10 got the banner and the REJECT rule counted 4 packets:
10 port 25
3 4 240 REJECT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25 #conn src/32 > 10 reject-with tcp-reset
Choose the limit with care on a Zimbra Open Source mail server. Many users behind one office NAT share an address, and a limit of 10 may block legitimate clients on 587 or 993. Apply a limit like this to port 25 only, as in the example, and raise it for your own traffic.
A tested ruleset for Zimbra Open Source
Here is the Zimbra Open Source ruleset I loaded in the lab, in the order that works. Loopback and established traffic come first, the connection limit sits before the port accept, the admin port is restricted to one admin address, and a rate-limited LOG rule goes last, before the default policy:
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 25 -m connlimit --connlimit-above 10 --connlimit-mask 32 -j REJECT --reject-with tcp-reset
iptables -A INPUT -p tcp -m multiport --dports 25,80,110,143,443,465,587,993,995 -j ACCEPT
iptables -A INPUT -s 10.77.0.20 -p tcp --dport 7071 -j ACCEPT
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT-DROP: "
iptables -P INPUT DROP
Add a rule for your own SSH source address before the policy line, or you will lock yourself out. Test result from the client at 10.77.0.20, and from a second client at 10.77.0.30:
port 25: Connection to 10.77.0.10 25 port [tcp/smtp] succeeded!
port 443: Connection to 10.77.0.10 443 port [tcp/https] succeeded!
port 587: nc: connect to 10.77.0.10 port 587 (tcp) failed: Connection refused
port 993: nc: connect to 10.77.0.10 port 993 (tcp) failed: Connection refused
port 7071: Connection to 10.77.0.10 7071 port [tcp/*] succeeded!
[second client 10.77.0.30] $ nc -zv -w2 10.77.0.10 7071
nc: connect to 10.77.0.10 port 7071 (tcp) timed out: Operation now in progress
Ports 587 and 993 now say "Connection refused" because the firewall lets the packets through and nothing in my lab listens there. On a real Zimbra Open Source server, the proxy or Postfix answers there. Port 7071 works only for the admin address, and the other client times out. The LOG rule counted the dropped packets (3 in the lab), and the limit match stops it from flooding the log. Tail the kernel log for lines starting with IPT-DROP.

Saving the rules and the DNAT rule from the old post
Firewall rules for Zimbra Open Source live in memory and vanish at reboot. iptables-save prints them in a form iptables-restore can load. In the lab I saved the rules to a file, flushed the chain, restored, and the policy and the first rules came back:
$ iptables-save > /root/rules.v4; iptables -F; iptables -P INPUT ACCEPT; iptables-restore < /root/rules.v4; iptables -S INPUT | head -3
-P INPUT DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Loading the file at boot needs the iptables-persistent package, which was not installed in my container (Installed: (none)), so I did not test the boot step. The old post also suggested a DNAT rule for "multi-domain" servers. It rewrites every packet for port 443 to another address. With that rule loaded, the client could no longer reach port 443 on the server:
$ iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 192.168.1.10:443
[client] $ nc -zv -w3 10.77.0.10 443
nc: connect to 10.77.0.10 port 443 (tcp) timed out: Operation now in progress
$ iptables -t nat -F
[client] $ nc -zv -w3 10.77.0.10 443
Connection to 10.77.0.10 443 port [tcp/https] succeeded!
Zimbra Open Source does not need NAT to host several domains on one server. Use DNAT only when you really forward port 443 to a different machine. For a longer treatment of rule order and counters, see my iptables how-to.
Questions about the Zimbra Open Source firewall
Can I use UFW instead of raw iptables? The old post said yes, but I did not test UFW with Zimbra, so I cannot confirm it. UFW writes iptables rules too, so the port list above still applies.
Should the admin console be public? The Zimbra wiki lists 7071 as an internal port, so a Zimbra Open Source admin console should not face the internet. Keep it limited to admin addresses or a VPN, as in the ruleset.
Which of these rules matter most? Established traffic, the complete port list and the order of the connection limit. Without them a Zimbra Open Source server either cannot call out or does not enforce its own limits.
Comments
Post a Comment