Stalwart repeatedly blocks Traefik’s IP for “Excessive port scanning attempts” despite being in AllowedIp

I run Stalwart behind Traefik using Proxy Protocol v2. Both containers are connected to the same Docker network:

  • Traefik: 172.20.0.2
  • Stalwart: 172.20.0.5

Since deploying this setup, Stalwart periodically becomes unusable because it blocks Traefik’s IP address. I must manually remove the block before mail services start working again:

stalwart-cli --url http://172.20.0.5:8080 query BlockedIp \
  --user admin --password XXX

The result is consistently similar to:

Id            IP Address(es)   Reason                                    Expires At   Created At
jdvnpaecaaqa  172.20.0.2       Excessive port scanning attempts          <none>       2026-09-08T12:00:18Z

The relevant part of my Stalwart Docker Compose configuration is:

mailserver:
  image: stalwartlabs/stalwart:latest
  container_name: mailserver
  restart: unless-stopped
  hostname: mail.${DOMAIN}

  networks:
    proxy:
      ipv4_address: 172.20.0.5

  labels:
    - traefik.enable=true

    # Web dashboard and JMAP discovery
    - traefik.http.routers.mailserver.rule=Host(`mail.${DOMAIN}`) || Host(`autodiscover.${DOMAIN}`) || Host(`autoconfig.${DOMAIN}`) || Host(`mta-sts.${DOMAIN}`)
    - traefik.http.routers.mailserver.entrypoints=external-websecure
    - traefik.http.routers.mailserver.service=mailserver
    - traefik.http.services.mailserver.loadbalancer.server.port=8080

    # SMTP with Proxy Protocol v2
    - traefik.tcp.routers.smtp.rule=HostSNI(`*`)
    - traefik.tcp.routers.smtp.entrypoints=smtp
    - traefik.tcp.routers.smtp.service=smtp
    - traefik.tcp.services.smtp.loadbalancer.server.port=25
    - traefik.tcp.services.smtp.loadbalancer.serversTransport=proxyprotocolv2@file

    # HTTPS/JMAP with TLS passthrough and Proxy Protocol v2
    - traefik.tcp.routers.jmap.rule=HostSNI(`*`)
    - traefik.tcp.routers.jmap.tls.passthrough=true
    - traefik.tcp.routers.jmap.entrypoints=external-websecure
    - traefik.tcp.routers.jmap.service=jmap
    - traefik.tcp.services.jmap.loadbalancer.server.port=443
    - traefik.tcp.services.jmap.loadbalancer.serversTransport=proxyprotocolv2@file

    # SMTPS with TLS passthrough and Proxy Protocol v2
    - traefik.tcp.routers.smtps.rule=HostSNI(`*`)
    - traefik.tcp.routers.smtps.tls.passthrough=true
    - traefik.tcp.routers.smtps.entrypoints=smtps
    - traefik.tcp.routers.smtps.service=smtps
    - traefik.tcp.services.smtps.loadbalancer.server.port=465
    - traefik.tcp.services.smtps.loadbalancer.serversTransport=proxyprotocolv2@file

    # IMAPS with TLS passthrough and Proxy Protocol v2
    - traefik.tcp.routers.imaps.rule=HostSNI(`*`)
    - traefik.tcp.routers.imaps.tls.passthrough=true
    - traefik.tcp.routers.imaps.entrypoints=imaps
    - traefik.tcp.routers.imaps.service=imaps
    - traefik.tcp.services.imaps.loadbalancer.server.port=993
    - traefik.tcp.services.imaps.loadbalancer.serversTransport=proxyprotocolv2@file

The relevant Traefik static configuration is:

entryPoints:
  external-web:
    address: ":8080"
    http:
      redirections:
        entryPoint:
          to: external-websecure
          scheme: https
          permanent: true

  external-websecure:
    address: ":8443"

  smtp:
    address: ":25"
    proxyProtocol:
      trustedIPs:
        - 172.20.0.2
        - 172.20.0.5

  smtps:
    address: ":465"
    proxyProtocol:
      trustedIPs:
        - 172.20.0.2
        - 172.20.0.5

  imaps:
    address: ":993"
    proxyProtocol:
      trustedIPs:
        - 172.20.0.2
        - 172.20.0.5

The dynamic Traefik configuration contains:

tcp:
  serversTransports:
    proxyprotocolv2:
      proxyProtocol:
        version: 2

My Stalwart listeners are configured as follows:

Listener Bind Use TLS TLS implicit Override Proxy Trusted Networks
http :8080 Disabled Disabled Empty
https :443 Enabled Enabled 172.20.0.2
smtp :25 Enabled Disabled 172.20.0.2
submissions :465 Enabled Enabled 172.20.0.2
imaptls :993 Enabled Enabled 172.20.0.2

I have followed the official Stalwart documentation for running behind Traefik at every step. What am I doing wrong? This behavior has made Stalwart effectively unusable, since I must manually unblock Traefik before the mail services work again. Any advice on where to look would be appreciated.

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

Traefik’s IP is being flagged by Stalwart’s auto-ban (portScanning) logic, which is a completely separate mechanism from the Proxy Protocol trusted-networks setting. overrideProxyTrustedNetworks on a listener only tells Stalwart to trust and parse the Proxy Protocol header from that address, it does not exempt that address from rate limiting or auto-banning.

The “excessive port scanning attempts” ban fires when a source repeatedly connects to TCP ports Stalwart isn’t listening on, or an HTTP client hits exploit-style paths matched by scanBanPaths (default threshold 30/day). Since Traefik is your reverse proxy, every health-check, container-to-container probe, or connection attempt on a port Stalwart isn’t actively serving (or matching a banned path glob) from 172.20.0.2 counts toward that same counter for that IP, and it gets banned outright, taking down all your legitimate proxied traffic since it all also arrives from 172.20.0.2.

The fix is to add Traefik’s container IP as an explicit AllowedIp entry (Security › Allowed IPs in the WebUI, or the AllowedIp object via CLI/JMAP), which bypasses rate limiting and auto-banning entirely for that address, rather than relying solely on the per-listener proxy-trust override:

{ "address": "172.20.0.2" }

Localhost is allowed by default for this reason; anything else in front of Stalwart (like Traefik) needs to be added manually.

Reference: Auto-banning and General server settings – IP address allow list.

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

The bot is correct, you can try those suggestions

I added Traefik’s IP (172.20.0.2) to AllowedIp immediately after reading the bot’s reply, but unfortunately it has not prevented Stalwart from banning it.

I’ve just confirmed via the CLI that the IP is present in the allow list

stalwart-cli --url http://172.20.0.5:8080 query AllowedIp --user admin --password XXXXX

Id            IP Address(es)  Reason for allowing  Expires At  Created At
jd7l2ytqaaqa  172.20.0.2      Traefik IP           <none>      2026-09-09T11:04:00Z

However, the same IP was subsequently banned for port scanning

stalwart-cli --url http://172.20.0.5:8080 query BlockedIp --user admin --password XXXXX

jd9mzjukacqa  172.20.0.2  Excessive port scanning attempts  <none>  2026-09-09T15:52:06Z

So 172.20.0.2 was explicitly allowlisted at 11:04, yet Stalwart still added it to BlockedIp at 15:52 the same day for Excessive port scanning attempts.

At this point, I’m totally clueless as to why it’s still getting banned. Nothing I’ve tried so far has worked. I’d really appreciate any targeted solution or guidance on what to check next.

Actions → Reload → Blocked IPs list

The allow list never reached the running server, which is why adding the entry changed nothing. Allowed addresses are compiled into the configuration at startup, and creating the object writes it to the registry without rebuilding anything. Run stalwart-cli create Action/ReloadSettings (or Actions, Reload, Server settings in the UI) and it takes effect; the same reload also clears the existing block, since the block check consults the allow list first. Blocked addresses have their own fast path that applies immediately, allowed ones do not, which is a rough edge on our side.

overrideProxyTrustedNetworks is stricter still: it is read when the listeners are spawned, so it needs a container restart and a settings reload will not do it. If you set those overrides in the WebUI and did not restart, Traefik’s Proxy Protocol header is reaching the SMTP parser as an invalid command, and each one of those counts against the same scanning threshold. Thirty in a day is ordinary mail volume, which would explain the roughly daily rhythm of the re-bans.

Underneath both of those is the attribution problem. Something is reaching Stalwart from Traefik with neither Proxy Protocol nor a forwarded header, so a stranger’s probe gets charged to 172.20.0.2. Your http listener on 8080 has no trusted networks set and useXForwarded is off by default; our Traefik page turns it on and your config does not show it. The path list bans on a single matching request and the ban period has no default, which is your Expires At: <none>.

I would not turn useXForwarded on to fix that, though. It is server-wide and we honour the header from any source without checking it came from a trusted network, so with your TLS passthrough router in front of 443 a client could hand you any address it liked. The safer shape is to stop serving 8080 through Traefik’s HTTP router at all and let the passthrough path that already carries Proxy Protocol serve the WebUI too.

The security.scan-ban lines in your log carry the address along with the path or reason that triggered them. Those will say in one line which of the two vectors above is yours.

Thanks for detailed reply, that cleared up many things. Might be a good idea to also drop the mailserver router from the official docs covering compose for stalwart with traefik. Traefik | Stalwart

@stalwart

Following the earlier recommendation, I removed the Traefik HTTP router that forwarded requests to Stalwart on port 8080. HTTPS traffic now uses a TCP router with TLS passthrough and Proxy Protocol v2:

# HTTPS: JMAP, WebUI, autodiscover/autoconfig and MTA-STS
- traefik.tcp.routers.mail-https.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-https.tls.passthrough=true
- traefik.tcp.routers.mail-https.entrypoints=external-websecure
- traefik.tcp.routers.mail-https.service=mail-https
- traefik.tcp.services.mail-https.loadbalancer.server.port=443
- traefik.tcp.services.mail-https.loadbalancer.serversTransport=proxyprotocolv2@file

# SMTP
- traefik.tcp.routers.smtp.rule=HostSNI(`*`)
- traefik.tcp.routers.smtp.entrypoints=smtp
- traefik.tcp.routers.smtp.service=smtp
- traefik.tcp.services.smtp.loadbalancer.server.port=25
- traefik.tcp.services.smtp.loadbalancer.serversTransport=proxyprotocolv2@file

# SMTPS
- traefik.tcp.routers.smtps.rule=HostSNI(`*`)
- traefik.tcp.routers.smtps.tls.passthrough=true
- traefik.tcp.routers.smtps.entrypoints=smtps
- traefik.tcp.routers.smtps.service=smtps
- traefik.tcp.services.smtps.loadbalancer.server.port=465
- traefik.tcp.services.smtps.loadbalancer.serversTransport=proxyprotocolv2@file

# IMAPS
- traefik.tcp.routers.imaps.rule=HostSNI(`*`)
- traefik.tcp.routers.imaps.tls.passthrough=true
- traefik.tcp.routers.imaps.entrypoints=imaps
- traefik.tcp.routers.imaps.service=imaps
- traefik.tcp.services.imaps.loadbalancer.server.port=993
- traefik.tcp.services.imaps.loadbalancer.serversTransport=proxyprotocolv2@file

This appears to have fixed the original client-IP attribution issue, Stalwart now records actual client IP addresses instead of Traefik’s container IP.

However, the change has broken my Bulwark webmail integration. Bulwark now reports:

The server is reachable but is blocking cross-origin requests. Check your JMAP server’s CORS settings and allow this domain.

In Stalwart under Settings → Services → HTTP → Security, both of these settings are enabled:

  • Enable HTTP Strict Transport Security
  • Permissive CORS policy

I have also confirmed that the client IP used to access Bulwark is not currently blocked by Stalwart. Is there a way to get around this?

Edit: I also found this guide: Auth events on HTTP layer use TCP remote IP instead of X-Forwarded-For client IP when behind reverse proxy · stalwartlabs/stalwart · Discussion #2806 · GitHub

It recommends routing MTA-STS, autoconfig, and autodiscover through an HTTP router to port 8080, while using TCP TLS passthrough only for the WebUI/JMAP hostname. This differs from the configuration you suggested, where a HostSNI('*') TCP router sent all HTTPS traffic to Stalwart’s port 443. However, I tested the guide’s split-routing configuration as well, and Bulwark reports the same CORS error with both setups.