Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Setting Up Proxmox Mail Gateway as a VM on Proxmox VE

profile picture
Author
Kürşad Ölmez
Published
2026-11-09
Time to read
36 minutes reading time

Introduction

Proxmox Mail Gateway (PMG) is an open-source mail filtering gateway that sits between the internet and your mail server, filtering spam, viruses, and other threats before they ever reach an inbox. It can be deployed either as a second layer of defense in front of an existing filtering service, or as the primary, direct-facing mail gateway that receives internet mail itself. This tutorial covers the latter: PMG configured as the mail server your domain's MX record points to directly, with a separate internal mail server behind it handling mailboxes.

This tutorial covers installing PMG as a virtual machine on a Proxmox VE host running on a Hetzner dedicated server, then walking through the core mail proxy settings that make it actually route and protect mail correctly: relay domains, transports, trusted networks, TLS, and DKIM signing, followed by the spam and virus filtering engines.

By the end of this tutorial, you will have:

  • A Proxmox Mail Gateway VM installed and running on Proxmox VE.
  • Mail Proxy settings configured so PMG only relays for your domains and correctly forwards filtered mail to your real mail server.
  • TLS and DKIM signing configured for outbound mail.
  • Spam and virus filtering enabled.

Prerequisites

  • A Hetzner dedicated server with Proxmox VE already installed - see Install and Configure Proxmox VE if you haven't set this up yet.
  • An additional public IPv4 address for the PMG VM. The server's main IP address stays with the Proxmox VE host, so order an additional single IPv4 address in Hetzner Robot: select your server, open the IPs tab, and click Order additional IPs/Nets at the bottom of the page. Hetzner asks for a technical reason for additional IPs (RIPE guidelines). If your Proxmox VE host uses the bridged network setup, also request a virtual MAC address for it in Robot (the small button next to the additional IP address) - without it, Hetzner may flag the traffic as abuse. If it uses the routed setup, no virtual MAC address is needed. Both setups are described in the Proxmox VE tutorial linked above.
  • An existing mail server (the one PMG will protect) reachable from the Proxmox VE host's network.
  • A domain you control, with access to its DNS zone.
  • Basic familiarity with Proxmox VE and mail server concepts (MX records, SMTP).

Example terminology

The <...> placeholders below are not real values - replace each one with your own domain, IP addresses, and passwords before running any command.

  • Domain: <example.com>
  • PMG hostname: mail-gateway.<example.com>
  • IP addresses (the same example values as in the Proxmox VE tutorial linked in the prerequisites):
    • Proxmox VE host (main IP of the dedicated server): <198.51.100.10>, its gateway: <198.51.100.1>
    • PMG VM (additional IP): <192.0.2.20>
    • Gateway of the additional IP (bridged setup only): <192.0.2.1>
    • Internal mail server (the mail server PMG forwards filtered mail to): <203.0.113.1>
  • Virtual MAC address: <virtual_mac> (bridged setup only, from Hetzner Robot)

Step 1 - Download the PMG ISO

On the Proxmox VE host, download the ISO directly into local ISO storage. Check the PMG downloads page first to confirm the current version and adjust the filename below if a newer release is available:

cd /var/lib/vz/template/iso/
wget https://enterprise.proxmox.com/iso/proxmox-mail-gateway_9.1-1.iso

Verify the download against the SHA256 checksum published on the downloads page:

sha256sum proxmox-mail-gateway_9.1-1.iso

Step 2 - Create the VM

PMG's official hardware requirements (recommended, for typical traffic) are a 64-bit multicore CPU, 4 GB RAM, and at least 8 GB of disk space. In practice, give it more disk than the bare minimum - the quarantine store grows over time, and you don't want to be resizing a disk under a production mail filter.

Before creating the VM, collect the network information you'll need for the installer in Step 3. Run these commands on the Proxmox VE host:

ip -br address show
ip route show default
cat /etc/network/interfaces
cat /etc/resolv.conf
pvesm status
  • ip -br address show lists the network interfaces and their addresses, including your bridge (usually vmbr0) and the host's main IP address (<198.51.100.10>).
  • ip route show default shows the default gateway of the host.
  • cat /etc/network/interfaces shows which setup the host uses. A bridge with bridge-ports none and up ip route add ... (or post-up ip route add ...) lines for the additional IPs is the routed setup. A bridge with bridge-ports set to your physical interface (for example enp0s31f6) is the bridged setup.
  • cat /etc/resolv.conf shows the DNS servers the host uses. You can enter one of them in the installer.
  • pvesm status lists your storages (e.g. local-lvm).

Then find the following in Hetzner Robot (your server, IPs tab): the additional IP address you will use as <192.0.2.20> and, for the bridged setup, its virtual MAC address (<virtual_mac>).

For the routed setup, also make sure IP forwarding is enabled on the host, as described in the Proxmox VE tutorial linked in the prerequisites:

sysctl net.ipv4.ip_forward

The output should be net.ipv4.ip_forward = 1.

The gateway for the PMG VM depends on your setup:

  • Routed: the main IP address of the Proxmox VE host (<198.51.100.10>).
  • Bridged: the gateway of the additional IP address (<192.0.2.1>). If the additional IP address is in the same subnet as the server's main IP address, this is the same gateway the host uses (shown by ip route show default).

Create the VM from the Proxmox VE shell. For the routed setup, use this:

qm create 200 \
  --name mail-gateway \
  --memory 4096 \
  --cores 2 \
  --net0 virtio,bridge=vmbr0 \
  --scsihw virtio-scsi-pci \
  --scsi0 local-lvm:20 \
  --ide2 local:iso/proxmox-mail-gateway_9.1-1.iso,media=cdrom \
  --boot order=ide2 \
  --ostype l26

For the bridged setup, set the virtual MAC address from Robot on the network card by replacing the --net0 line with:

  --net0 virtio=<virtual_mac>,bridge=vmbr0 \

Adjust 200 (the VM ID), local-lvm (your storage), vmbr0 (the bridge from your host's configuration), and the disk size to match your own environment. Check that the network card was created as intended:

qm config 200 | grep net0

Start the VM and open its console from the Proxmox VE web interface (Datacenter → your node → 200 (mail-gateway) → Console):

qm start 200

Step 3 - Install PMG

The installer is largely the same Debian-based wizard used by Proxmox VE and Proxmox Backup Server:

  1. Accept the license agreement.

  2. Select the target disk (the VM's virtual disk - this will be wiped).

  3. Choose your country, time zone, and keyboard layout.

  4. Set a root password and administrator email address (this address doesn't need to work yet, but PMG uses it for local system notifications - you can change it later).

  5. Configure the network. The installer asks for the hostname, IP address, gateway, and DNS server. Enter the values you collected in Step 2:

    Field Routed setup Bridged setup
    Hostname (FQDN) mail-gateway.<example.com> mail-gateway.<example.com>
    IP Address (CIDR) <192.0.2.20>/32 <192.0.2.20>/32
    Gateway <198.51.100.10> (main IP of the host) <192.0.2.1> (gateway of the additional IP)
    DNS Server A DNS server from /etc/resolv.conf on the host A DNS server from /etc/resolv.conf on the host

    During installation you can specify either an IPv4 or an IPv6 address, not both. Use IPv4 here and add IPv6 after the installation if you need it.

  6. Review the summary and click Install.

When the installation finishes, the installer asks you to reboot. The VM is still set to boot from the CD-ROM drive (the boot order from Step 2), so a plain reboot would start the installer again. On the Proxmox VE host, stop the VM, remove the ISO, set the boot order to the virtual disk, and start the VM again:

qm stop 200
qm set 200 --ide2 none
qm set 200 --boot order=scsi0
qm start 200

Step 4 - First Login

Open https://<192.0.2.20>:8006 in your browser (note the port - it shares 8006 with Proxmox VE's own interface, but this is a separate machine) and log in as root with the password you set during installation. Your browser will warn about the self-signed certificate - this is expected and fine for now; if you plan to access this interface securely from outside your network later, PMG has a built-in ACME client under Configuration → Certificates that can issue a trusted Let's Encrypt certificate without needing to obtain one elsewhere.

If the web interface doesn't load, the network configuration from Step 3 is the most likely cause. Open the VM's console in the Proxmox VE web interface (Datacenter → your node → 200 (mail-gateway) → Console), log in as root, and check the VM's network:

ip -br address show
ip route show default
ping -c 3 <198.51.100.10>

In the ping command, use the gateway you entered in Step 3 (<198.51.100.10> in the routed setup, <192.0.2.1> in the bridged setup). The address shown must be <192.0.2.20> and the default route must point to that gateway. PMG stores the network setup in /etc/network/interfaces, so you can correct it there and reboot the VM. Also make sure that /etc/hosts contains a line with <192.0.2.20> followed by mail-gateway.<example.com> and the short hostname mail-gateway.

While you troubleshoot, you can also reach the web interface through an SSH tunnel via the Proxmox VE host. Run this on your local computer:

ssh -L 8080:<192.0.2.20>:8006 root@<198.51.100.10>

Then open https://localhost:8080/ in your local browser.

You'll see a banner about the missing subscription - this is expected and doesn't affect functionality. PMG is fully open-source and usable without a subscription; the enterprise repository (and its faster-updated packages) is what a subscription unlocks. Don't run apt update yet - the enterprise repository is enabled by default and will fail without a subscription key, as covered in the next step.

The web interface (port 8006) and SSH (port 22) are now reachable on <192.0.2.20> from the internet. Restrict both to your own IP addresses, for example with the Proxmox VE firewall for this VM or the Hetzner Robot firewall, and keep port 25 open for incoming mail. PMG also needs several outgoing connections, such as DNS, signature updates, and Razor2 on TCP port 2703. Check the firewall settings in the admin guide before you restrict outgoing traffic.

Step 5 - Switch to the No-Subscription Repository

From this step on, run all commands in this tutorial on the PMG VM, not on the Proxmox VE host. Open the VM's console in the Proxmox VE web interface (Datacenter → your node → 200 (mail-gateway) → Console), or log in with SSH. Connect to the Proxmox VE host first and from there to the VM:

Replace <198.51.100.10> with your main IPv4, and <192.0.2.20> with your additional IPv4.

ssh root@<198.51.100.10>
ssh root@<192.0.2.20>

If you don't have a Proxmox subscription, the default enterprise repository will return an authentication error on every apt update until you either add a subscription key or switch to the free no-subscription repository. For most Hetzner deployments without a Proxmox support contract, disable it:

echo "Enabled: false" >> /etc/apt/sources.list.d/pmg-enterprise.sources

In this file format, an empty line starts a new block, so the new line must follow the existing lines directly. Check the result:

cat /etc/apt/sources.list.d/pmg-enterprise.sources

Enabled: false should be the last line, with no empty line before it.

Setting Enabled: false rather than deleting or commenting out the file matters for two reasons: commenting out individual lines in this newer deb822 format can produce a "malformed entry" error instead of just disabling the source, and if you activate a subscription later, re-enabling access is as simple as removing this one line - no need to recreate the file from scratch.

Now add the no-subscription repository:

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

Note that this repository is served over plain HTTP rather than HTTPS - that's expected and documented behavior from Proxmox, not a mistake in this tutorial; the packages themselves are still cryptographically signed and verified via the Signed-By key regardless of the transport.

Update and upgrade the system:

apt update && apt dist-upgrade -y

If you do have a Proxmox subscription, skip the Enabled: false step above, and instead activate your subscription key under Configuration → Subscription in the web interface before running apt update.

Step 6 - Configure Relaying

Go to Configuration → Mail Proxy → Relaying. The key setting here is Default Relay: the internal mail server that inbound mail for the domains you list in Relay Domains (Step 7) gets delivered to, once PMG has filtered it. If a single internal mail server handles all your Relay Domains, setting it here is usually simpler than defining every domain individually under Transports.

  • Default Relay: <203.0.113.1> - your internal mail server's address.

  • Relay Port: 25

  • Relay Protocol: smtp

  • Disable MX Lookup: No

  • Smarthost: leave empty, unless your hosting provider or ISP requires you to route PMG's own outbound connections (to the wider internet, not to your internal server) through their smarthost - some networks block direct outbound port 25 for anti-spam reasons. This is a separate concern from Default Relay above. When set, enter it as <IP_address>:<port> or <FQDN>:<port>, for example <smtp.example.net>:587 or <203.0.113.10>:25.

    One limitation worth knowing about: the GUI's Smarthost field has no place to enter a username or password. If your smarthost provider requires SMTP authentication (many do, especially on port 587), you need to configure Postfix SASL authentication manually on the command line, and persist it through PMG's Service Configuration Templates mechanism - anything you change directly in /etc/postfix/main.cf gets silently overwritten the next time PMG rewrites its configuration from the GUI.

    Make sure the SASL modules Postfix needs for authentication are installed:

    apt update
    apt install libsasl2-modules -y

    Create a password map file with your smarthost's credentials, in the format [host]:port username:password:

    cd /etc/postfix
    touch relay_passwd
    chmod 0600 relay_passwd
    echo "[<smtp.example.net>]:587 <username>:<password>" > relay_passwd
    postmap hash:relay_passwd

    The hash: prefix matches the map type used in main.cf below.

    Rather than editing /etc/postfix/main.cf directly, copy PMG's own template to the override location and edit it there, so your changes survive the next GUI-triggered rewrite:

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

    Add the following lines at the end of /etc/pmg/templates/main.cf.in. If a parameter is defined more than once, Postfix uses the last definition, so lines at the end take precedence over earlier values in the template:

    relayhost = [<smtp.example.net>]:587
    smtp_sasl_auth_enable = yes
    smtp_sasl_password_maps = hash:/etc/postfix/relay_passwd
    smtp_sasl_security_options = noanonymous
    smtp_tls_security_level = encrypt

    Apply the template and restart Postfix:

    pmgconfig sync --restart 1

    Leave the Smarthost field in the GUI empty when doing this - having both set at once causes conflicting configuration. Your copy in /etc/pmg/templates/ replaces the default template completely, so it doesn't receive changes Proxmox makes to the default template in later updates. After PMG upgrades, compare the two files and carry over relevant changes:

    diff /var/lib/pmg/templates/main.cf.in /etc/pmg/templates/main.cf.in

    One more thing worth knowing: /etc/postfix/relay_passwd (and its hashed .db file) is a plain file PMG has no awareness of, so it's never touched by pmgconfig sync or package upgrades - but it's also not included in PMG's own backup tool (pmgbackup), which only covers /etc/pmg/, mail filter rules, and the statistics database. If you ever migrate this installation to a new server using PMG's built-in backup and restore, remember to copy /etc/postfix/relay_passwd (and re-run postmap on the target, or copy the .db file too) separately - it won't come across automatically.

If PMG sits in front of more than one internal mail server - for example, if you're using it as a shared gateway protecting several separate customers, each with their own mail server - Default Relay only covers the common case. Any Relay Domain whose mail needs to go somewhere other than Default Relay needs an explicit entry under Transports (Step 10), which takes priority over the default for that specific domain.

Step 7 - Configure Relay Domains

Go to Configuration → Mail Proxy → Relay Domains and add every domain you want PMG to accept and relay mail for:

<example.com>

This is the setting that keeps PMG from becoming an open relay - PMG only accepts mail addressed to domains listed here. Any domain not listed is rejected during the SMTP conversation, before the message body is even transferred.

Step 8 - Configure Ports

Go to Configuration → Mail Proxy → Ports:

  • External SMTP Port: 25 - the port PMG listens on for mail arriving from the internet.
  • Internal SMTP Port: 26 (default) - the port your internal mail server should use to hand its outbound mail to PMG for relaying.

Leave these at their defaults unless you have a specific conflict on this VM.

Step 9 - Configure Options

Go to Configuration → Mail Proxy → Options. A few settings worth configuring deliberately here:

  • SMTP HELO Checks: Yes - rejects connections whose HELO/EHLO greeting is missing, malformed, or not a fully-qualified hostname. Real mail servers always identify themselves properly here; spam software frequently doesn't bother. This setting matters specifically because PMG is the mail server that first receives the connection directly from the internet in this setup, rather than sitting behind another filtering layer that would have already screened out the worst offenders - PMG needs to do this screening itself.

  • Use SPF: Yes - checks the connecting server's IP against the SPF record published in DNS for the claimed sender domain, and rejects mail when that check fails, meaning the sending IP isn't authorized by the domain's SPF record. It is enabled by default. Like HELO checks, this only makes sense to enable when PMG is the server actually receiving the connection (so it sees the real originating IP) - if a gateway upstream of PMG were relaying mail to it, PMG would see that gateway's IP instead of the true sender, and SPF checks would compare the wrong address against the DNS record, either failing legitimate mail or letting through mail it shouldn't. Since this deployment has PMG accepting mail directly from the internet, that concern doesn't apply.

  • Reject Unknown Senders: Yes - rejects mail whose sender domain has no valid DNS (no MX record and no A record) at all, meaning it can't possibly be a real, reachable domain. This is a soft check (a temporary 4xx rejection, so a legitimate sender with a brief DNS hiccup isn't permanently lost) that catches a meaningful share of spam from forged or entirely made-up sender domains, at a low false-positive cost.

  • Reject Unknown Clients: generally leave this No, despite it sounding like a natural companion to the setting above. It rejects connections from IP addresses that have no reverse DNS (PTR) entry, or whose PTR hostname doesn't resolve back to the same IP address. Some legitimate mail servers have missing or mismatched reverse DNS, so this strict check rejects real mail more often than the softer checks above.

  • Hide Internal Hosts: Yes - strips your internal mail server's hostname and IP out of the Received headers on outgoing mail. Without this, every message leaving your network reveals a piece of your internal network topology to whoever inspects the headers - not a critical vulnerability on its own, but unnecessary information disclosure with no real benefit to hand out.

  • SMTPD Banner: customize this to something that doesn't reveal the mail server software or version, for example just your hostname. The default banner otherwise identifies enough for a scanner or attacker to fingerprint what's running before attempting anything else - minor hardening, but a cheap one.

  • Receiver Verification: Yes (550) - PMG checks with your internal mail server that a recipient address actually exists before accepting the message. Without this, PMG accepts mail for addresses that don't exist and then has to bounce it after the fact, which is how servers end up sending "backscatter" spam to forged senders. The setting offers two reply codes to send back to the connecting sender when a recipient turns out not to exist:

    • 550 - a permanent rejection. The sender's mail server gets an immediate, definitive "no such user" and won't retry. This is the standard choice once you've confirmed verification is working correctly.
    • 450 - a temporary rejection. The sender's mail server queues the message and retries later. Useful briefly while first setting this up, since a misconfiguration on your internal mail server's side (for example, if it's slow to respond or briefly unreachable) causes a deferral instead of a hard, incorrect bounce of legitimate mail.

    Either way, this only works correctly if your internal mail server actually rejects unknown recipients itself - if it silently accepts mail for any address and generates its own bounce afterward, PMG's verification probe will report every address as valid, and this setting won't filter anything.

  • Greylisting: enabled by default for IPv4. Here's how it actually works, per Proxmox's own documentation: the first time PMG sees mail from a given <sender network, sender email, recipient email> combination (referred to as a "triplet"), it temporarily rejects the message with a 450 code rather than accepting it. Legitimate mail servers treat this as a normal transient failure and retry automatically after a delay; most spam-sending software doesn't queue and retry at all, so a meaningful share of spam simply never comes back. Once a triplet successfully retries, PMG remembers it, and future mail matching that same triplet passes through immediately - so the delay only ever affects the first message from a new contact, not every message from them.

    The practical tradeoff is that first-contact delay, which the documentation describes as usually under 30 minutes, though in practice most legitimate mail servers retry within minutes. This occasionally surprises people expecting instant delivery from someone new. A useful escape hatch: mail from senders you explicitly trust (via the SMTP Welcomelist) skips greylisting entirely, so you're not stuck choosing between "greylisting everyone" and "greylisting no one." If the first-contact delay doesn't suit your use case, disable it here.

  • DNSBL Sites: a list of DNS blocklists that Postfix's postscreen checks the IP address of every connecting server against, before the SMTP conversation starts. A server whose combined score reaches the DNSBL Threshold (default 1) is rejected. Because postscreen only looks up IP addresses, only IP-based blocklists work here.

    A note specific to Hetzner customers: Spamhaus has publicly stated that Hetzner's network prevents them from attributing DNSBL queries to individual users, so since February 2025 they've been progressively blocking free public-mirror queries (plain zen.spamhaus.org and similar) from Hetzner IP ranges, returning an error code (127.255.255.254) instead of real results. If your mail server doesn't handle that error code correctly, this can result in mail being rejected outright rather than just silently skipping the check. The fix is to register for Spamhaus's free Data Query Service (DQS) instead of using the public mirror:

    1. Sign up for a free DQS account and verify your email.
    2. Log in to the Spamhaus portal and copy your DQS key.
    3. Use the key-prefixed zone <your_dqs_key>.zen.dq.spamhaus.net instead of zen.spamhaus.org.

    Spamhaus also offers domain-based lists through DQS, such as dbl (domain blocklist) and zrd (zero-reputation domains). Don't add those to DNSBL Sites: they list domain names, not IP addresses, so postscreen can't use them. Spamhaus documents using them with Postfix's sender and HELO domain checks instead (Configuring DQS on Postfix). In PMG, that requires a template customization like the one in Step 6 and is beyond the scope of this tutorial.

    Combine this with a couple of free, registration-free zones as well - bl.mailspike.net and psbl.surriel.com are both still active and don't require signing up for anything:

    <your_dqs_key>.zen.dq.spamhaus.net
    bl.mailspike.net
    psbl.surriel.com

    Adding more zones isn't automatically better - several forum threads from long-time PMG admins note that stacking in less-maintained or inconsistent lists mostly adds false positives rather than catching more spam. Add zones deliberately, one at a time, watching for false positives before adding the next.

    One thing worth knowing before you rely on this: several DNSBL providers rate-limit queries per public IP address, and if your PMG VM resolves DNS through your ISP's nameserver or a public resolver like 8.8.8.8 or 1.1.1.1, you're sharing that quota with everyone else using the same resolver - meaning DNSBL lookups can silently start failing under load, showing up in mail logs or SpamAssassin hits as URIBL_BLOCKED, RCVD_IN_DNSWL_BLOCKED, or SURBL_BLOCKED. Proxmox's own documentation recommends running a local recursive resolver (they use Unbound) directly on the PMG VM so its DNSBL queries go out under your own dedicated public IP instead of a shared one:

    apt install unbound bind9-dnsutils
    dig a proxmox.com @127.0.0.1 +short

    The dig command here is a quick sanity check, not part of the actual configuration - it asks the resolver you just installed (@127.0.0.1) to look up proxmox.com's IP address itself. Getting back an IP address, rather than a timeout or error, confirms Unbound is listening and resolving correctly before you point PMG at it.

    Then point PMG's own DNS resolution at it, either via Configuration → Network/Time → DNS in the web interface (set DNS Server 1 to 127.0.0.1) or by editing /etc/resolv.conf directly, and restart the mail services so the change takes effect:

    systemctl restart pmg-smtp-filter pmgpolicy postfix

    See the linked wiki page for more advanced setups, such as forwarding your own internal domain's lookups to an internal DNS server while still resolving DNSBL zones recursively.

    One important distinction: running your own Unbound resolver solves the shared-quota problem above, but it does not solve the Hetzner-specific Spamhaus block described earlier - that block is based on which network your queries originate from, and a resolver running on the same Hetzner VM still queries from a Hetzner IP either way. The only fix for that specific issue is switching to the DQS key-based zones.

Step 10 - Configure Transports

Go to Configuration → Mail Proxy → Transports. Use this when a specific Relay Domain needs to go to a different internal mail server than the one set as Default Relay in Step 6 - this is exactly the case when PMG is a shared gateway in front of several separate mail servers rather than a single one:

  • Relay Domain: <example.com>
  • Host: <203.0.113.1> (this domain's specific mail server - different from Default Relay)
  • Protocol: smtp (or lmtp if that domain's mail server expects LMTP - if so, note that Use MX becomes irrelevant and is disabled automatically)
  • Port: the port that mail server listens on for incoming relayed mail (often 25, sometimes a dedicated submission port depending on that server's own configuration)
  • Use MX: No - you're pointing directly at a specific host, not looking up its MX records

Add one entry per domain that needs to diverge from Default Relay. Domains not listed here simply fall through to whatever you set as Default Relay in Step 6.

Step 11 - Configure Networks

Go to Configuration → Mail Proxy → Networks and add your internal mail server's IP (or the network it's on) as a trusted network:

<203.0.113.1>/32

Hosts listed here are allowed to relay outbound mail through PMG. Without this, your internal mail server's own outbound mail (the mail it sends, as opposed to mail PMG delivers to it) has nowhere authorized to go through PMG's proxy. If you configured multiple internal mail servers under Transports in Step 10, add each of their IPs here individually - each one needs to be explicitly trusted, not just the one set as Default Relay. Hosts in the same subnet as PMG itself are allowed to relay by default, without being listed here.

Step 12 - Configure TLS

Go to Configuration → Mail Proxy → TLS:

  • Enable TLS: Yes
  • Enable TLS Logging: Yes - useful for troubleshooting delivery issues later without having to turn logging on retroactively.
  • Add TLS received header: Yes

When you enable TLS, PMG generates a self-signed certificate automatically for encrypting SMTP sessions (/etc/pmg/pmg-tls.pem) - this is separate from the web interface's own certificate, and generally doesn't need replacing, since mail servers negotiating opportunistic TLS don't validate the certificate chain the way a browser does.

If you have specific partner domains that should only ever be reached over TLS (never falling back to plaintext), add a TLS Destination Policy entry for that domain.

Step 13 - Configure DKIM

Go to Configuration → Mail Proxy → DKIM:

  • Enable DKIM Signing: Yes
  • Selector: default (or any short identifier of your choosing - this becomes part of the DNS record name later). PMG stores the private key for the selector in /etc/pmg/dkim/<selector>.private.
  • Sign all Outgoing Mail: No - PMG then signs only mail from the domains in the DKIM domain list on this page, or from your Relay Domains if that list is empty. Add <example.com> to that list. With Yes, PMG would sign all outgoing mail, including mail from domains whose DNS doesn't publish your DKIM key (for example other customers' domains when PMG is shared between several mail servers, as described in Step 6), and those signatures would fail verification.
  • Select Signing Domain: the default envelope signs with the domain of the envelope sender. The Proxmox documentation notes that header (the domain in the From: header) is useful because some mail, such as bounces, has an empty envelope sender, and DMARC relies on the From: header domain.

Click View DNS Record to see the TXT record you need to add in Step 16.

Step 14 - Configure the Spam Detector Engine

Go to Configuration → Spam Detector → Options. This page configures the SpamAssassin engine itself - which detection techniques it uses and how - rather than what PMG actually does with a given spam score (that part lives in Mail Filter → Rules, covered separately, since it's a rule-based system rather than a single threshold setting).

  • Use auto-welcomelists: Yes - enables SpamAssassin's Auto-Welcomelist plugin, which adjusts a message's score based on the historical reputation of that specific sender/IP combination with your system. A sender you've received legitimate mail from repeatedly gets a small trust bonus over time.
  • Use Bayesian filter: Yes - a statistical classifier that learns from mail your system has already seen, improving accuracy over time based on what's been marked spam versus legitimate in your environment.
  • Use RBL checks: Yes - enables SpamAssassin's own real-time blocklist lookups as part of content scoring. This is a separate mechanism from the DNSBL Sites you configured under Mail Proxy in Step 9: that one rejects mail outright at the SMTP level before it's even queued, while this one contributes to the overall spam score during content analysis. Both are worth having enabled; they catch overlapping but not identical things. SpamAssassin's built-in Spamhaus tests query the public Spamhaus mirrors, so on Hetzner they are affected by the blocking described in Step 9 and won't return useful results. The other RBL tests still work.
  • Use Razor2 checks: Yes (default) - enables Vipul's Razor2, a collaborative database of spam fingerprints. Checking mail against Razor2 doesn't require registration (only reporting spam to it does), but Razor2 connects to its servers on TCP port 2703, so outgoing connections on that port must be allowed. Its default scoring weight is quite low out of the box. If you want Razor2 hits to meaningfully affect filtering, increase RAZOR2_CF_RANGE_51_100 and RAZOR2_CHECK under Configuration → Spam Detector → Custom Scores (a score of 8 is a reasonable starting point used by other admins), then click Apply Scores.
  • Extract Text from Attachments: No is the default, and reasonable to leave as-is unless you have a specific reason to change it. Enabling this extracts and scans text from attached doc, pdf, rtf, and image files for spam content - useful for catching spam hidden inside attachments, but meaningfully more CPU-intensive per message.
  • Max Spam Size (bytes): 262144 (256 KB) by default - the maximum message size that gets full content analysis. Messages larger than this skip content scanning (though they're still subject to the SMTP-level checks from Step 9), since scanning very large messages in full is expensive for comparatively little anti-spam benefit - spam is rarely large.
  • Languages: all by default. Restricting this to specific language codes tells SpamAssassin which languages are expected for legitimate mail, which factors into scoring - only worth narrowing if you're confident you'll never legitimately receive mail in other languages.
  • Backscatter Score: 0 by default, meaning it's effectively off. Backscatter is what happens when someone forges your domain as the sender of spam, and the (unwitting) receiving servers bounce that mail back - to you, since you appear to be the sender. If you want PMG to specifically score these bounce-notification patterns higher, this needs a non-zero value; pair it with the wl_bounce_relays option in /etc/pmg/pmg.conf (legitimate bounce relays, listed with the Spam Detector options in the admin guide) for your own outbound relays first, so real bounces of mail you actually sent aren't caught by the same net.
  • Heuristic Score: 3 by default - this feeds in scoring from ClamAV's heuristic detections (encrypted archives or documents, and scanned phishing URLs) into the overall spam score, bridging a virus-engine signal into spam scoring rather than keeping the two entirely separate.

PMG's default rules already act on the resulting score: mail with a spam score of 3 or higher goes to the spam quarantine. To change this, for example to block very high scores outright, edit the rules under Mail Filter → Rules using Spam Level as the condition - this is outside the scope of this tutorial.

Step 15 - Configure the Virus Detector

Go to Configuration → Virus Detector → Options. PMG scans mail with ClamAV. The fields on this Options tab are mostly about how deeply archives get inspected and what counts as suspicious on its own:

  • Block encrypted archives and documents: Yes (the default is No) - ClamAV can't look inside an encrypted archive or document. With this enabled, such files are marked as a heuristic match. Despite the name, this doesn't block the mail by itself: it raises the spam score by the Heuristic Score from Step 14 (default 3), which with the default rules is enough to move the mail to the spam quarantine. Malware is often sent in password-protected archives to avoid scanning, so enabling this is reasonable.
  • Max recursion: 5 - how many levels deep PMG will unpack nested archives (a zip inside a zip inside a zip, and so on) before it stops. This exists as zip-bomb protection - without a limit, a maliciously crafted, deeply nested archive could consume enormous CPU and memory trying to fully unpack.
  • Max files: 1000 - the maximum number of files PMG will extract from a single archive to scan. Same purpose as Max recursion: bounding resource usage against archives crafted to contain an excessive number of files.
  • Max file size: 25000000 (25 MB) - files larger than this aren't scanned. Combined with Max scan size below, this keeps a single oversized attachment from tying up the scanner disproportionately.
  • Max scan size: 100000000 (100 MB) - the total amount of data scanned per input file overall, across everything unpacked from it. This is the aggregate cap; Max file size limits any one file within an archive, while this limits the sum total.
  • Max credit card numbers: 0 (disabled) by default. This isn't classic virus detection - it's a basic data-loss-prevention check that generates a detection when a file contains at least this many credit card or social security numbers, useful for catching, for example, someone accidentally emailing a spreadsheet full of customer card numbers. Leave it at 0 unless you specifically want this kind of outbound content scanning; a low nonzero value (like 1 or 2) enables it.

By default, PMG moves mail with a virus finding to the virus quarantine, where an administrator can review it and deliver it in case of a false positive; users are not notified. If you prefer to block virus mail outright, change the virus rule under Mail Filter → Rules.

Extending ClamAV with Third-Party Signature Feeds (Optional)

ClamAV's own official signatures (main.cvd/daily.cld, updated via database.clamav.net) are general-purpose. Several community and vendor-maintained feeds add signatures focused specifically on things that show up disproportionately in email - phishing, malicious macros, malware hidden in archives - and can meaningfully improve detection beyond the defaults. Well-known ones include R-fx Networks (rfxn), Sanesecurity, and InterServer, among others.

Adding one is done the same way as the SASL configuration in Step 6: since freshclam.conf is also managed by PMG's config engine and gets rewritten on sync, edit it through the template override rather than directly:

mkdir -p /etc/pmg/templates
cp /var/lib/pmg/templates/freshclam.conf.in /etc/pmg/templates/freshclam.conf.in

Add one or more DatabaseCustomURL lines to the end of /etc/pmg/templates/freshclam.conf.in, for example:

DatabaseCustomURL http://www.rfxn.com/downloads/rfxn.ndb
DatabaseCustomURL http://www.rfxn.com/downloads/rfxn.hdb

Apply the change:

pmgconfig sync --restart 1

Then trigger a database update under Configuration → Virus Detector → ClamAV (or wait for the next automatic update) and check that the new databases appear in the list there. As with main.cf.in in Step 6, compare your copy with /var/lib/pmg/templates/freshclam.conf.in after PMG upgrades.

Rather than listing every available signature file here, Sanesecurity publishes a reference table of each database they (and several affiliated projects) produce, what it targets, and its approximate false-positive risk - use that to pick the specific feeds that make sense for your mail volume and risk tolerance, rather than adding everything at once. That table lists database names (for example junk.ndb), not full download URLs - you build the actual DatabaseCustomURL by combining a mirror's base address with the name, for example https://mirror.rollernet.us/sanesecurity/junk.ndb. If you'd rather see a set of already-complete URLs as a starting point, the Gentoo wiki's ClamAV Unofficial Signatures page lists several worked examples in exactly that format. A couple of things worth knowing before relying on any of these: they're unofficial and unaffiliated with the ClamAV project itself, so check each provider's own usage terms before adding it; and since most are distributed through community mirrors rather than a single canonical source, an individual mirror can occasionally go down or move - keep an eye on freshclam's update logs if detection quality seems to quietly regress over time.

Step 16 - Update DNS Records

Once mail routing works, update your domain's DNS zone:

  • A record: mail-gateway.<example.com> pointing to <192.0.2.20>. The MX record must point to a hostname, so create this first.
  • MX: point <example.com>'s MX record at mail-gateway.<example.com>, replacing (or standing alongside, at a lower priority during testing) whatever it previously pointed to.
  • SPF: update your SPF TXT record to include PMG's public IP, so receiving servers know mail from PMG is authorized to send on your domain's behalf.
  • DKIM: add the TXT record for the selector you chose in Step 13, in the form <selector>._domainkey.<example.com>, with the value shown by View DNS Record in Step 13.
  • DMARC: if you don't already have a DMARC record, this is a good point to add one, now that SPF and DKIM are both in place.

Also set the reverse DNS (PTR) entry for <192.0.2.20> to mail-gateway.<example.com> in Hetzner Robot: open your server's IPs tab and enter the hostname in the text box next to the IP address. Many receiving mail servers check that the sending IP address has a reverse DNS entry that matches its hostname.

Step 17 - Validate

  • Sending a test message from an external address to a mailbox on <example.com> arrives, and shows PMG's hostname in the Received headers.
  • Sending a test message from a mailbox on <example.com> to an external address arrives, and the external side shows a valid DKIM signature (many providers, including Gmail, show this under "message details" or similar).
  • Attempting to relay mail through PMG from an external host (not from a network listed under Networks) to a domain not in Relay Domains is rejected.
  • postconf smtpd_helo_restrictions on the PMG VM lists HELO checks, confirming the SMTP HELO Checks option from Step 9 took effect.
  • dig -x <192.0.2.20> +short returns mail-gateway.<example.com>.
  • With TLS logging enabled, the Tracking Center shows TLS details for incoming and outgoing connections after some traffic has passed through.

Conclusion

You now have Proxmox Mail Gateway running as a VM on Proxmox VE, filtering mail for your domain before it reaches your real mail server, with TLS and DKIM signing configured for outbound mail. From here, review the default rules under Mail Filter → Rules and adjust them to your needs, and check the quarantines regularly to tune false positives.

Next steps:

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue

Get new tutorials via RSS / Atom

Discover our

Dedicated Servers

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Configure now
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more
Report Issue

Get new tutorials via RSS / Atom