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:
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.
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.
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
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.
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
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:
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?
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.