Proxy Protocol enabled for trusted networks, but SMTP submissions listener reports "invalid proxy header"

Your question

Hi,

I’m running into an issue with Stalwart where a previously working configuration suddenly stopped working.

The submissions listener (port 465) is now logging repeated Proxy Protocol errors:

2026-06-30T09:35:58Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"
2026-06-30T09:36:01Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"
2026-06-30T09:36:01Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"

The confusing part is that this listener is configured to trust Proxy Protocol connections from my Kubernetes Pod CIDR and Service CIDR:

❯ stalwart-cli query NetworkListener
Id            Name         Protocol     Bind addresses      Implicit TLS
iwefv2ikahqb  http         HTTP         [::]:80             No
iwefv2icahab  https        HTTP         [::]:443            Yes
iwefv2h7agqb  sieve        ManageSieve  [::]:4190           No
iwefv2hsagab  pop3s        POP3         [::]:995            Yes
iwefv2hkafqb  imaps        IMAP4        [::]:993            Yes
iwefv2hcafab  submissions  SMTP         [::]:465            Yes
iwefv2g7aeqb  smtp         SMTP         [::]:25             No
❯ stalwart-cli get NetworkListener iwefv2hcafab
Listener settings
  Listener Id:     submissions
  Protocol:        SMTP
  Bind addresses:  [::]:465
  Max Connections: 8192

TLS options
  Enable TLS:            Yes
  Implicit TLS:          Yes
  Disabled Protocols:    <none>
  Disabled Ciphersuites: <none>
  Handshake Timeout:     1 m
  Ignore client order:   Yes

Proxy networks
  Override proxy networks: 10.42.0.0/16, 10.43.0.0/16

Socket options
  Backlog:         1024
  TTL:             <none>
  Type of Service: <none>
  Send buffer:     <none>
  Receive buffer:  <none>
  No delay:        Yes
  Reuse Address:   Yes
  Reuse port:      Yes

The X-Forwarded-For header is only enabled for HTTP.

❯ stalwart-cli get Http
Response
  Headers:       <none>
  Redirect root: /account

Proxy
  Obtain remote IP from Forwarded header: Yes

Security
  Allowed endpoints:
    Expression
      Conditions:
        <empty>
      Default Value: 200
  Enable HTTP Strict Transport Security: No
  Permissive CORS policy:                No

Rate Limit
  Authenticated:
    Rate
      Count:  1000
      Period: 1 m
  Anonymous:
    Rate
      Count:  100
      Period: 1 m

Even when I create a pod in the same Kubernetes cluster and connect directly to the service, I cannot establish a TLS connection on port 465:

kubectl run alpine --image=alpine:latest --restart=Never -it --rm -- sh
/ # apk add openssl
(1/1) Installing openssl (3.5.7-r0)
Executing busybox-1.37.0-r31.trigger
OK: 13.8 MiB in 26 packages
/ # openssl s_client -connect stalwart.stalwart.svc.cluster.local:465
Connecting to 10.43.12.94
CONNECTED(00000003)
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
Negotiated TLS1.3 group: <NULL>
---
SSL handshake has read 0 bytes and written 1568 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
~ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
364: eth0@if365: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether aa:fd:ed:3e:fa:33 brd ff:ff:ff:ff:ff:ff
    inet 10.42.4.87/32 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::a8fd:edff:fe3e:fa33/64 scope link
       valid_lft forever preferred_lft forever
~ # nc -v stalwart.stalwart.svc.cluster.local 465
stalwart.stalwart.svc.cluster.local (10.43.12.94:465) open
~ # nc -v stalwart.stalwart.svc.cluster.local 25
stalwart.stalwart.svc.cluster.local (10.43.12.94:25) open
220 mail.example.com Stalwart ESMTP at your service
~ #

The client pod has the IP address 10.42.4.87, which falls within my configured trusted proxy network (10.42.0.0/16).

It appears the connection is closed before the TLS handshake begins, as if the listener is expecting a Proxy Protocol header even when it shouldn’t, or is incorrectly determining that the client should be using Proxy Protocol.

This configuration had been working previously and I don’t changed anything related to the listener configuration before this started happening.

Is the current behavior expected? Any guidance would be appreciated.

The key behavior is clear from the source code. Let me now check if there’s any nuance around IPv4-mapped IPv6 addresses in the trusted network matching:

Now I have a clear picture of the behavior. The source code confirms the exact issue. Let me also check the IPv4-mapped IPv6 handling for the proxy network matching:

The source code makes the behavior very clear. Here is what’s happening:

When a connection arrives from an IP that is in the proxy_networks (trusted) list, Stalwart unconditionally attempts to parse a Proxy Protocol header from the incoming TCP stream – every time, with no fallback. This is by design: the server calls ProxiedStream::create_from_tokio(stream, ...) and if that fails (because the client sent a TLS ClientHello instead of a PROXY header), it logs the "invalid proxy header" warning and drops the connection.

In other words, the trusted-networks list does not mean “accept Proxy Protocol from these addresses if they send it.” It means “require Proxy Protocol from these addresses.” Any connection from a trusted CIDR is expected to carry a Proxy Protocol header; connections that do not (like your direct openssl s_client test pod) will always fail.

This explains everything you’re seeing:

  • Direct connections from within 10.42.0.0/16 (your Pod CIDR) fail because those pods don’t prepend a PROXY header – Stalwart expects one and can’t parse the raw TLS handshake as a PROXY header.
  • Port 25 works from inside the cluster because the smtp listener apparently has no trusted proxy networks configured (or a different set), so it processes the connection directly.

To fix this, you need to ensure that every connection reaching the submissions listener on port 465 from the trusted CIDRs (10.42.0.0/16, 10.43.0.0/16) actually comes through a reverse proxy that emits a PROXY Protocol header. If you want to allow both direct and proxied connections on the same port, that is not currently supported – you would need a separate listener for direct internal connections with no proxy networks configured.

Reference: Proxy Protocol configuration docs

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

Even when I remove the Override proxy networks: <none> config the error is still the same:

❯ stalwart-cli get NetworkListener iwefv2hcafab
Listener settings
  Listener Id:     submissions
  Protocol:        SMTP
  Bind addresses:  [::]:465
  Max Connections: 8192

TLS options
  Enable TLS:            Yes
  Implicit TLS:          Yes
  Disabled Protocols:    <none>
  Disabled Ciphersuites: <none>
  Handshake Timeout:     1 m
  Ignore client order:   Yes

Proxy networks
  Override proxy networks: <none>

Socket options
  Backlog:         1024
  TTL:             <none>
  Type of Service: <none>
  Send buffer:     <none>
  Receive buffer:  <none>
  No delay:        Yes
  Reuse Address:   Yes
  Reuse port:      Yes
2026-06-30T10:20:38Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"
2026-06-30T10:20:38Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"
2026-06-30T10:20:39Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"
2026-06-30T10:20:40Z WARN Proxy protocol error (network.proxy-error)
	listenerId = "submissions"
	localIp = ::
	localPort = 465
	tls = true
	reason = "invalid proxy header"

Update:

Reinstalling the server with the exact same configuration resolved the issue, but I don’t know why. Since the configuration didn’t change, I’m concerned the underlying problem still exists and may reoccur. Is there any way to determine what caused it?

This is expected. Because the pod IP (10.42.4.87) is inside the trusted proxy CIDR, Stalwart requires a PROXY-protocol header on that connection, and a bare openssl s_client or nc connect sends none, so it’s rejected with “invalid proxy header” before TLS even starts. It isn’t the listener misbehaving.

Clearing the per-listener overrideProxyTrustedNetworks doesn’t disable proxy protocol either: an empty override falls back to the server-wide proxyTrustedNetworks, which still covers the pod CIDR. The intended setup is that only your load balancer’s address is in proxyTrustedNetworks, and that balancer prepends the PROXY header; direct in-cluster clients should be outside the trusted range (or speak PROXY protocol themselves). The “reinstall fixed it” was almost certainly a different/empty trusted-networks set afterwards, so check the current proxyTrustedNetworks lists only your real proxy addresses. Docs: Proxy Protocol | Stalwart .