Best practices for running Stalwart behind an inbound mail relay

Your question

What are the best practices of configuring Stalwart to accept inbound emails from Antivirus & Spam filtering relay?
Sender → Antivirus relay → Stalwart → Recipient

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Here is a consolidated set of best practices for running Stalwart behind an inbound AV/spam filtering relay (the “Sender → AV relay → Stalwart → Recipient” topology):


1. Preserve the real client IP via Proxy Protocol

The AV relay is just another SMTP proxy. Without knowing the original sender’s IP, Stalwart’s SPF/DMARC checks, rate-limiting, and auto-banning all run against the relay’s IP, which is useless (and will self-ban your relay).

  • Configure the relay to emit Proxy Protocol headers on its connections to Stalwart.
  • On Stalwart’s listener, enable Proxy Protocol acceptance and mark the relay’s IP as a trusted network.

Docs: Reverse proxy overview - Preserving the client IP and Proxy Protocol


2. Use a dedicated listener for the relay

Create a separate SMTP listener (e.g. on a private interface or non-standard port) exclusively for your AV relay. This lets you write targeted expressions for just that listener, e.g.:

{"name": "relay-in", "protocol": "smtp", "bind": ["127.0.0.1:10025"]}

Docs: Listeners


3. Disable Stalwart’s built-in spam filter for the relay listener

Since the AV relay has already scanned the message, you don’t want Stalwart to re-scan it. The enableSpamFilter field in the DATA stage accepts an expression - scope it to skip your relay listener:

{
  "enableSpamFilter": {
    "match": [{"if": "listener == 'relay-in'", "then": "false"}],
    "else": "is_empty(authenticated_as)"
  }
}

Docs: Inbound DATA - Spam filtering


4. Relax or bypass SPF/DKIM/DMARC checks for the relay

SPF will legitimately fail when the relay re-sends mail, since its IP is not in the sender’s SPF record. Use the remote_ip or listener variables in the SenderAuth expressions (spfVerify, dkimVerify, dmarcVerify) to set them to disable or relaxed for connections from the relay.

Docs: DMARC Verification, SenderAuth object


5. Restrict the relay-in listener to the relay’s IP only

Use the isClientAllowed / connection-stage expression or firewall rules to ensure only your relay can connect to this listener - preventing it from becoming an open relay. If you want Stalwart to accept relayed mail (to local recipients) from the relay without authentication, adjust allowRelaying scoped to that listener’s IP:

{
  "allowRelaying": {
    "match": [{"if": "listener == 'relay-in' && remote_ip == '10.0.0.1'", "then": "true"}],
    "else": "!is_empty(authenticated_as)"
  }
}

Docs: Inbound RCPT - Relay


6. (Optional) Trust X-Spam headers from the relay

If your AV relay adds X-Spam-Status or similar headers, you can write a Sieve rule or MTA Hook to consume those and act on them instead of running Stalwart’s own filter.

Docs: MTA Hooks, Sieve scripting


In summary: create a dedicated listener for the relay, enable Proxy Protocol so the real sender IP flows through, scope spam/auth checks to skip that listener, and lock down allowRelaying to the relay’s IP only.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

@helpbot

Here’s what i’ve done:

  1. Disabled SPF Verification for mails coming from AV relay
    MTA → Sender Authentication → SPF Verification
    EHLO & MAIL FROM:
    if: local_port == 25 && remote_ip != ‘av-relay-ip’
    then: relaxed
  2. Disabled Spam filtering for mails coming from AV relay
    MTA → DATA Stage → Spam filtering
    if: remote_ip == ‘av-relay-ip’
    then: false

After this configurations i’m successfully receiving mails from AV relay with `X-AntiSpam-Status` header, but now i have another question. How to make Stalwart to put mails that marked as spam with `X-AntiSpam-Status` header to Junk folder? Is using System Sieve script right way? What are the steps to achieve described behavior?

No, you need to use a user sieve script (uploaded via ManageSieve or JMAP for Sieve) if you want to change where a message is stored.