Swiss...
Proxmox Mail Gateway with Carbonio: Building a Hardened Swiss Email Filtering Architecture
Step-by-step deployment of a dedicated inbound filtering layer with hardening, DNSBL-ready DNS, SPF filtering and real-world testing.
June 11, 2026
by SwissLayer 17 min read
Proxmox Mail Gateway filtering inbound email in front of a Carbonio mail server on Swiss infrastructure

Email remains the single most abused entry point into any organization. Extortion campaigns, credential phishing, malware droppers, spoofed invoices — the overwhelming majority of it arrives over SMTP, and it arrives at whatever server your MX record points to. If that record points directly at your production mailbox server, then your production mailbox server is doing edge duty on the open internet: parsing hostile input, absorbing connection floods, and evaluating every forged sender the planet can produce.

At SwissLayer we run our own mail infrastructure on Carbonio, hosted entirely on our own Proxmox VE cluster in a Swiss datacenter. For years the mailbox server was its own front line. It handled the job, but the architecture bothered us: a single system was simultaneously the internet-facing SMTP endpoint, the spam filter, the antivirus scanner, the IMAP/POP server, the webmail platform and the storage backend. Every one of those roles has a different threat model, and stacking them on one box means a problem in any layer can reach all of them.

This article documents — completely, command by command, including the things that went wrong — how we deployed Proxmox Mail Gateway (PMG) 9.0 as a dedicated inbound filtering layer in front of Carbonio. Almost every PMG guide online assumes Microsoft Exchange or a generic Postfix backend; the Carbonio integration has real gotchas that nobody has written up properly, and we hit several of them. We also hit an IPv6 performance trap, a hosting provider silently blocking port 25, and a DNS resolver subtlety that silently guts spam detection if you miss it. All of it is below.

The Architecture: An Inbound-Only Filtering Gateway

Proxmox Mail Gateway is a full mail proxy: it terminates inbound SMTP connections, runs every message through SpamAssassin 4 and ClamAV, evaluates SPF, DKIM and DMARC, queries DNS blocklists, applies a rule engine, and only then relays accepted mail to the real mail server behind it. It is open source (AGPLv3), built on Debian 13, and managed through the same style of web interface as Proxmox VE — which made it a natural fit for our existing cluster.

The key design decision was to deploy it inbound-only:

Inbound mail: Internet → MX → PMG (port 25) → filter → Carbonio (port 25)
Outbound mail: Carbonio sends directly to the internet, exactly as before
Client submission: Mail clients authenticate to Carbonio on port 587, never touching PMG
IMAP / POP / Webmail: Clients connect to Carbonio directly, unchanged

Why inbound-only? Because it changes nothing about deliverability. Our SPF records, PTR records and DKIM signing all stay exactly as they are — the sending path is untouched, so there is zero risk of suddenly landing in customers' spam folders because a new hop appeared in the outbound chain. The gateway does one job: it stands between the internet and the mailbox server, and everything hostile stops there.

A second consequence of this design is enforceable at the network layer: if the gateway only ever needs to deliver mail to one internal host, it can be firewalled so that it is physically incapable of sending mail anywhere else. Even a full compromise of the gateway cannot turn it into a spam cannon. We implement exactly that below.

Deploying the VM on Proxmox VE

PMG ships as an ISO installer. We pulled the current release directly onto the Proxmox node's ISO storage and verified the checksum against the value published on the official download page:

wget https://enterprise.proxmox.com/iso/proxmox-mail-gateway_9.0-1.iso -P /var/lib/vz/template/iso/
sha256sum /var/lib/vz/template/iso/proxmox-mail-gateway_9.0-1.iso
# expected: 23bf1e50bba06f6f850360740c0388462b921b414d2e4f3ea859e5150f498c8e

The VM itself is deliberately small: 2 vCPU, 32 GB disk on Ceph-backed storage, virtio-scsi, and a single virtio NIC on the infrastructure VLAN with a dedicated public IP (we will call it 192.0.2.22, gateway 192.0.2.21). During installation, the FQDN you enter becomes the SMTP banner that every mail server in the world will see — we used mailgateway.swisslayer.com and published a matching A record before installing.

One sizing lesson worth its own paragraph: we initially booted the VM with 2 GB of RAM, and pmgversion immediately complained:

system memory size of 1.9 GiB is below the recommended 4+ GiB limit!

That warning is not cosmetic. ClamAV alone holds well over a gigabyte resident once its signature database loads, and SpamAssassin, PostgreSQL and the PMG API daemons all want their share. A filtering gateway that swaps under load will time out SMTP sessions. We shut the VM down, raised it to 6 GB, and started it again:

qm set 103 --memory 6144
qm shutdown 103
qm start 103

Plan for 4 GB minimum; 6 GB gives comfortable headroom.

Repositories and First Update

A fresh PMG install points at the enterprise repository, which returns HTTP 401 without a subscription. PMG 9 uses the modern deb822 .sources format, so we disabled the enterprise repo by adding one line to its file:

# /etc/apt/sources.list.d/pmg-enterprise.sources
Types: deb
URIs: https://enterprise.proxmox.com/debian/pmg
Suites: trixie
Components: pmg-enterprise
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
Enabled: false

And created the no-subscription repository alongside it:

# /etc/apt/sources.list.d/pmg-no-subscription.sources
Types: deb
URIs: http://download.proxmox.com/debian/pmg
Suites: trixie
Components: pmg-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg

Then the full upgrade:

apt update
apt dist-upgrade -y

In our case this brought 160 package upgrades including a new kernel, taking PMG from 9.0.1 to 9.0.14. And it was during this very first download that the deployment produced its first war story.

The IPv6 Trap: Diagnosing a 15 kB/s Mail Gateway

The repository indexes — 17 MB of metadata — took nineteen minutes to download. The 633 MB package set was estimating eight hours. Three independent mirrors (Proxmox, Debian, Debian Security) were all crawling at roughly 15 kB/s, which immediately rules out "the mirror is slow" and points at something local to the VM's network path.

We split the problem in half. From the Proxmox node itself:

wget -O /dev/null http://deb.debian.org/debian/dists/trixie/main/binary-amd64/Packages.gz
# 12.71M in 0.02s — 625 MB/s

The node was flying. The same download inside the VM crawled at 14.7 kB/s. So: node fast, VM slow, same physical uplink. We checked for an accidental rate limit on the virtual NIC (qm config 103 | grep net — none set), and then the wget output itself gave the answer away:

Resolving deb.debian.org (deb.debian.org)... 2a04:4e42:600::644, 2a04:4e42:200::644, ...
Connecting to deb.debian.org (deb.debian.org)|2a04:4e42:600::644|:80... connected.

The VM had picked up an IPv6 address via SLAAC from the VLAN's router advertisement and was preferring IPv6 for every connection — while the node had tested over IPv4. The IPv6 path was degraded (the classic symptom set of a path MTU blackhole: connections establish instantly, bulk transfer dies in retransmissions). The IPv4 path was perfect.

For a mail gateway, our calculus was simple: the MX record would publish an A record only, all our tooling and ACLs are IPv4, and a half-working IPv6 path would actively damage DNSBL lookups and delivery reliability. We disabled IPv6 on the VM entirely:

# /etc/sysctl.d/99-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
sysctl --system

Download speed instantly returned to full line rate. But disabling IPv6 under a mail server has a follow-up consequence that will bite anyone who skips it.

Postfix and the Template System: Doing Config Changes the PMG Way

Postfix's compiled default is inet_protocols = all. With IPv6 removed from the kernel, a Postfix instance still configured for "all" will eventually fail to bind its IPv6 sockets — typically at the worst possible moment, after a reboot. The fix is one line, but where you put that line matters enormously on PMG.

PMG generates /etc/postfix/main.cf from its own template system. Edit the generated file directly and your change will be silently overwritten on the next configuration sync. The correct procedure is to copy the stock template into the override directory and edit it there:

mkdir -p /etc/pmg/templates
cp /var/lib/pmg/templates/main.cf.in /etc/pmg/templates/main.cf.in
nano /etc/pmg/templates/main.cf.in

At the top of the override template we added:

# IPv6 disabled on this host
inet_protocols = ipv4

Then regenerated the live configuration:

pmgconfig sync --restart 1
grep inet_protocols /etc/postfix/main.cf
# inet_protocols = ipv4

One more subtlety from the logs: Postfix refuses to apply an inet_protocols change on reload — it explicitly warns to change inet_protocols, stop and start Postfix. A full restart (or in our case, the reboot already owed for the new kernel) is required before the change takes effect.

After the reboot, a port audit gave us our clean baseline:

ss -tlnp

0.0.0.0:25 — Postfix smtpd, the public MX listener
0.0.0.0:26 — PMG's internal Postfix instance (never exposed externally)
*:8006 — pmgproxy, the web management interface
0.0.0.0:22 — SSH
127.0.0.1:10022-10025 — the filter pipeline (pmgpolicy, pmg-smtp-filter, reinjection)
127.0.0.1:5432 — PostgreSQL, localhost only

Everything stateful was already correctly bound to localhost. One exception: rpcbind was listening publicly on port 111 with no role whatsoever on a mail gateway. Gone:

systemctl disable --now rpcbind.socket rpcbind.service

Hardening: Firewall, Fail2ban and Kernel

Our standard hardening stack applies to every server we operate, and the gateway is no exception. The firewall policy expresses the inbound-only architecture directly. Note the ordering of the last two rules — the specific outbound allow must precede the general outbound deny:

ufw default deny incoming
ufw default allow outgoing
ufw allow in 25/tcp
ufw allow from 203.0.113.10 to any port 22 proto tcp
ufw allow from 203.0.113.20 to any port 22 proto tcp
ufw allow from 203.0.113.10 to any port 8006 proto tcp
ufw allow from 203.0.113.20 to any port 8006 proto tcp
ufw allow from 198.51.100.10 to any port 161 proto udp
ufw allow out to 192.0.2.30 port 25 proto tcp
ufw deny out 25/tcp
ufw enable

Reading that policy top to bottom: SMTP is open to the world (it is the MX — it has to be), SSH and the management UI answer only to two trusted administration IPs, SNMP answers only to our monitoring server, and — the part we consider the signature of this design — the gateway may open outbound port 25 connections to exactly one destination: the Carbonio server (192.0.2.30). A global deny then blocks port 25 to everywhere else. Even if an attacker fully owned this VM, it could not deliver spam to the internet. The compromise would be loud, contained, and useless to them.

Fail2ban watches SSH via the systemd journal:

# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw

[sshd]
enabled = true
backend = systemd
systemctl enable --now fail2ban
fail2ban-client status sshd

And the kernel gets our standard network hygiene — strict reverse-path filtering, SYN cookies, redirect and source-route rejection, martian logging:

# /etc/sysctl.d/99-hardening.conf
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
sysctl --system

Defense in depth continues one layer up. On the distribution switch, the VLAN's ingress ACL ends in an explicit deny, and the gateway earned exactly two new entries: SMTP from anywhere, SNMP from the monitoring host. Nothing else needed adding — return traffic for the VM's own outbound connections, DNS and NTP replies, and ICMP for path MTU discovery were already covered by the ACL's standing rules:

ip access-list INFRA-EDGE-IN
 remark --- PMG mail gateway (192.0.2.22) ---
 permit tcp any host 192.0.2.22 eq smtp
 remark SNMP polling from monitoring
 permit udp host 198.51.100.10 host 192.0.2.22 eq snmp

Two independent layers — the switch ACL and the host firewall — each enforcing the same minimal surface. Either one failing still leaves the other standing.

The DNS Detail That Silently Breaks Spam Filtering

This section is the one we most wish someone had written before we started running mail servers years ago. A filtering gateway lives and dies by DNS blocklist (DNSBL) lookups. When a server connects to deliver mail, SpamAssassin fires queries like 22.2.0.192.zen.spamhaus.org to ask Spamhaus whether the connecting IP is a known spam source. Those answers are among the strongest signals in the entire scoring system.

Here is the trap: Spamhaus and most major DNSBLs block or rate-limit queries arriving from large public resolvers like 8.8.8.8 and 1.1.1.1. Your query arrives at their servers from Google's resolver fleet, indistinguishable from millions of others, and gets refused. The failure is completely silent — mail keeps flowing, messages keep getting scored, and the strongest anti-spam signal just quietly returns nothing. You will spend weeks wondering why obvious spam scores 2.1 instead of 12.

The fix is to run your own recursive resolver on the gateway itself, so queries leave from your own IP directly to the authoritative servers:

apt install -y unbound
# /etc/unbound/unbound.conf.d/local.conf
server:
 interface: 127.0.0.1
 access-control: 127.0.0.0/8 allow
 do-ip6: no
 prefetch: yes
systemctl enable --now unbound

And — critically — verify it. Spamhaus publishes permanent test records for exactly this purpose. The address 127.0.0.2 is always listed; 127.0.0.1 is never listed. A correct setup returns answers for the first and NXDOMAIN for the second:

dig @127.0.0.1 2.0.0.127.zen.spamhaus.org +short
# 127.0.0.10
# 127.0.0.4
# 127.0.0.2 <-- listed in SBL, XBL and PBL: lookups work

dig @127.0.0.1 1.0.0.127.zen.spamhaus.org +short
# (empty / NXDOMAIN — the negative control)

Point the system resolver at unbound (nameserver 127.0.0.1 in /etc/resolv.conf, or via the PMG web interface under Configuration → Network/Time → DNS), and one final gotcha: restart the filter daemon. SpamAssassin's resolver library reads the DNS configuration at process start. Our filter had booted before the resolver change, and its very first scored message carried the tell-tale hit RCVD_IN_DNSWL_BLOCKED — a DNS-based list refusing the query. One restart later:

systemctl restart pmg-smtp-filter

…the same lookup returned a clean RCVD_IN_DNSWL_NONE. If you take one operational habit from this article: after any resolver change on a mail gateway, restart the filter and verify the hits in the next scored message.

Wiring the Gateway to Carbonio: Relay Domains and Transports

With the platform hardened, the actual mail configuration is almost anticlimactic. In the PMG web interface:

Configuration → Mail Proxy → Relay Domains: add each domain the gateway should accept mail for
Configuration → Mail Proxy → Transports: for each domain, set the destination host (the Carbonio server, 192.0.2.30), port 25, protocol smtp

We started with a single low-stakes test domain rather than production domains — a decision we recommend without reservation, as the next section will demonstrate. Behind the scenes PMG writes these settings to its own configuration files and compiles the Postfix transport map. Worth knowing for verification: on PMG 9, Postfix is pointed directly at PMG's file rather than the traditional /etc/postfix/transport:

postconf transport_maps
# transport_maps = hash:/etc/pmg/transport

grep -r . /etc/pmg/transport
# testdomain.net smtp:[192.0.2.30]:25ls -la /etc/pmg/transport*
# /etc/pmg/transport (source)
# /etc/pmg/transport.db (compiled map)

On the Carbonio side, the domain and a mailbox must exist, or Carbonio will bounce everything the gateway relays. Carbonio's provisioning CLI runs as the zextras user:

su - zextras
carbonio prov cd testdomain.net
carbonio prov ca user@testdomain.net 'StrongPasswordHere'
carbonio prov gd testdomain.net | head -5

First Contact: The Gateway Rejects Its Own Administrator

For testing we used swaks, the Swiss Army knife of SMTP testing, aimed directly at the gateway's IP — deliberately before changing any MX records, so that any failure would be configuration, not DNS propagation.

Two testing realities surfaced immediately, and both are worth knowing before you burn an evening on them. First: many VPN and hosting providers silently block outbound port 25 at their network edge as anti-spam policy. Our first test attempts simply hung at the TCP connect — a packet capture on the gateway (tcpdump -ni ens18 port 25) showed zero packets arriving, and tracing the path back revealed the VPN provider's edge eating them. If you test SMTP through a VPN or from a VPS, verify the provider passes port 25 at all before debugging your own stack.

Second — and this one made the deployment: our first successful connection was rejected by the gateway itself.

swaks --to user@testdomain.net --from admin@ourcompany.com --server 192.0.2.22 --port 25

<** 554 5.7.1 <user@testdomain.net>: Recipient address rejected:
 Rejected by SPF: 203.0.113.10 is not a designated mailserver
 for admin@ourcompany.com (context mfrom, on mailgateway...)

Read that carefully: we claimed to be our own company's mail address while connecting from a residential IP that is not in our SPF record. The gateway evaluated the SPF policy, concluded the message was a forgery — which, technically, it was — and refused it at the RCPT stage, before a single byte of message content was transmitted. The very first SMTP conversation the gateway ever had was a spoofing attempt, and it blocked it. No tuning, no training, default ruleset.

The legitimate version of the test, sent from a server that genuinely is a designated sender for the domain, sailed through. The log trace shows PMG's two-instance architecture in action — the message is accepted under one queue ID, scored, then re-injected as a new queue ID for delivery:

postfix/smtpd: ED04E1E02C7: client=mail.ourcompany.com[192.0.2.30]
pmg-smtp-filter: SA score=0/5 time=1.102
 hits=DMARC_PASS(-0.1),SPF_HELO_PASS(-0.001),SPF_PASS(-0.001),...
pmg-smtp-filter: accept mail to <user@testdomain.net> (34AB41E048B) (rule: default-accept)
postfix/smtp: 34AB41E048B: to=<user@testdomain.net>,
 relay=192.0.2.30[192.0.2.30]:25, dsn=2.0.0, status=sent

The Carbonio Gotcha: Trusting the Gateway

Except — our first relayed message did not actually reach the inbox. PMG accepted it, scored it, relayed it, and then Carbonio bounced it:

status=bounced (host 192.0.2.30 said:
 553 5.7.1 <admin@ourcompany.com>: Sender address rejected:
 not logged in (in reply to RCPT TO command))

This is the integration detail that generic PMG guides never cover, because Exchange and vanilla Postfix do not behave this way. Carbonio (inheriting the behavior from its Zimbra lineage) enforces an anti-spoofing rule: any message whose envelope sender belongs to a domain Carbonio hosts must arrive over an authenticated session. The gateway's relay connection is, by design, unauthenticated — so any external message that legitimately carries one of your own domains as sender (mailing list traffic, certain forwarders, and of course test messages) gets refused.

The correct fix is to add the gateway's IP to Carbonio's trusted networks, which tells its Postfix layer that this peer is part of the mail infrastructure. The attribute is replace-not-append, so read the current value first and re-set it in full with the gateway added:

su - zextras
carbonio prov gs mail.ourcompany.com zimbraMtaMyNetworks
# zimbraMtaMyNetworks: 127.0.0.0/8 [::1]/128 192.0.2.28/30 ...

carbonio prov ms mail.ourcompany.com zimbraMtaMyNetworks \
 '127.0.0.0/8 [::1]/128 192.0.2.28/30 192.0.2.22/32'

One more modernization note: the traditional zmmtactl restart is deprecated — current Carbonio releases manage services through systemd targets. As root:

systemctl restart carbonio-mta.target
/opt/zextras/common/sbin/postconf mynetworks
# mynetworks = 127.0.0.0/8 [::1]/128 192.0.2.28/30 192.0.2.22/32

With the gateway trusted, the same test completed end to end: accepted on the gateway, scored, relayed, accepted by Carbonio, delivered to the inbox. The Tracking Center — PMG's searchable per-message audit trail — now told the whole story in three consecutive rows: rejected (the SPF forgery), accepted/bounced (the untrusted relay), accepted/delivered (the working pipeline). We could not have scripted a better demonstration of each layer doing its job.

Going Live: MX Cutover and Real-World Traffic

Cutover is deliberately boring: lower the domain's DNS TTL in advance, then point the MX record at the gateway's hostname.

testdomain.net. MX 10 mailgateway.swisslayer.com.

Mail routing and everything else about the domain are independent: the webmail A record, IMAP endpoints and client configurations all continue pointing at Carbonio, untouched. Senders that cached the old MX simply keep delivering directly to Carbonio until their TTL expires — that path still works during the overlap, so the cutover has no hard edge.

Minutes after the record propagated, the first genuine internet message arrived — sent from a Gmail account, received by Google's outbound fleet, delivered to our gateway. The score line shows the full modern authentication stack evaluated against a real-world sender:

pmg-smtp-filter: SA score=0/5 time=1.262
 hits=DKIM_SIGNED(0.1),DKIM_VALID(-0.1),DKIM_VALID_AU(-0.1),
 DKIM_VALID_EF(-0.1),DMARC_PASS(-0.1),FREEMAIL_FROM(0.001),
 HTML_MESSAGE(0.001),RCVD_IN_DNSWL_NONE(-0.0001),
 RCVD_IN_MSPIKE_H2(0.001),SPF_HELO_NONE(0.001),SPF_PASS(-0.001)
postfix/smtp: relay=192.0.2.30:25, dsn=2.0.0, status=sent

DKIM validated, DMARC passed, the sending IP checked against reputation lists, total processing time 1.4 seconds — and the message appeared on a phone over Carbonio's IMAP exactly as it always had. The clients never learned that anything changed.

For production domains, the same playbook repeats with one addition we deliberately deferred: Carbonio runs its own amavis-based spam and virus layer inherited from Zimbra, and once the gateway filters in front of it, double-filtering only doubles the false-positive surface. Before moving production traffic, relax Carbonio's own filtering and let the gateway be the single scoring authority — and only after all MX records have moved, restrict Carbonio's port 25 to accept connections exclusively from the gateway's IP, closing the direct path permanently.

Swiss Hosting Advantages: Filtering Without Surrendering Your Mail

There is a structural difference between this architecture and the way most organizations consume email filtering today. The dominant commercial model — cloud filtering services and hosted email security gateways — works by pointing your MX records at someone else's infrastructure, usually in a foreign jurisdiction. Every message your organization receives, including the legitimate ones containing contracts, health information, financial records and privileged correspondence, is processed, parsed and often retained on systems outside your control before you ever see it.

For organizations subject to the Swiss Federal Act on Data Protection (FADP), or handling data where banking secrecy, attorney-client privilege or medical confidentiality applies, that model is increasingly difficult to justify. The revised FADP places direct obligations on how personal data is processed and transferred abroad — and email is personal data in nearly every message.

The architecture documented here keeps the entire mail path under Swiss jurisdiction:

Data sovereignty: Every message is received, filtered, scored and stored on infrastructure physically located in a Swiss datacenter, operated under Swiss law — no transit through foreign cloud filtering services
FADP alignment: No third-party processor sits in the inbound mail path; there is no offshore data processing agreement to negotiate because there is no offshore processing
Full audit capability: The Tracking Center provides a complete, searchable record of every message decision — accepted, quarantined, rejected and why — on your own systems, which is exactly the evidence trail compliance reviews ask for
Open source, no black box: Every component — PMG, Postfix, SpamAssassin, ClamAV, Unbound — is open source software whose filtering behavior can be inspected, tuned and audited, in contrast to proprietary cloud filters whose decision logic is invisible
No per-mailbox tax: Commercial gateways price per user per month, forever; this entire deployment consumes one small VM on infrastructure you already operate

This is the same philosophy behind everything we build at SwissLayer: compliance-sensitive workloads deserve infrastructure where you can point at every component, name its jurisdiction, and audit its behavior.

Implementation Checklist

✅ Allocate a dedicated public IP and publish an A record for the gateway's FQDN before installing
✅ Deploy the PMG ISO on a VM with 2 vCPU, minimum 4 GB RAM (6 GB recommended), 32 GB disk
✅ Switch to the no-subscription repository and run a full dist-upgrade plus reboot
✅ Verify download throughput — if it crawls, check whether the VM is preferring a broken IPv6 path
✅ If disabling IPv6: set inet_protocols = ipv4 via the PMG template override, never by editing main.cf directly, and fully restart Postfix
✅ Disable rpcbind and any other service with no role on a mail gateway
✅ Firewall: port 25 open to the world; management ports (22, 8006) restricted to trusted IPs; SNMP restricted to monitoring
✅ Enforce inbound-only at the host firewall: allow outbound 25 only to the mail server, deny outbound 25 to everything else
✅ Mirror the minimal surface in the upstream switch/router ACL for defense in depth
✅ Deploy fail2ban (systemd backend) and kernel network hardening via sysctl
✅ Install Unbound as a local recursive resolver and verify with the Spamhaus 127.0.0.2 test record — both positive and negative controls
✅ Restart pmg-smtp-filter after any resolver change and verify no _BLOCKED hits in the next scored message
✅ Configure relay domains and per-domain transports to the backend mail server
✅ Create the domain and mailboxes on Carbonio; verify with carbonio prov gd
✅ Add the gateway's IP to zimbraMtaMyNetworks and restart carbonio-mta.target
✅ Test with swaks directly against the gateway IP before touching DNS — and test from a network that actually passes outbound port 25
✅ Verify the full pipeline in the Tracking Center: accept, score, relay, delivery confirmation
✅ Cut over MX records one domain at a time, lowest-traffic first, with lowered TTLs
✅ Send a real external test (Gmail) and inspect the authentication results in the score line
✅ Before production cutover: relax the backend's own spam filtering to avoid double-scoring
✅ After all domains are moved: lock the backend's port 25 to accept only the gateway's IP

Conclusion

One evening of focused work produced a filtering architecture that blocked its first spoofing attempt before any tuning, survives the compromise scenarios that matter, keeps every message under Swiss jurisdiction, and cost nothing beyond a small VM on a cluster we already run. The mistakes along the way — the IPv6 trap, the resolver subtlety, the Carbonio trust relationship — are precisely the details that separate a deployment that works from one that silently underperforms for months, and they are now documented here so you can skip them.

If you are building email infrastructure that has to answer to regulators, auditors or simply your own standards for data sovereignty, the foundation matters as much as the software. SwissLayer operates exactly this class of infrastructure — Swiss datacenters, owned network, transparent architecture — and we build and manage deployments like this one for compliance-sensitive clients. Explore our dedicated servers in Switzerland or get started today with a Swiss VPS, and run your mail on infrastructure you can actually point to.