Stalwart behind ferron proxy

how to config stalwart to be used behind ferron, and please add ferron web server configuration guide to doc site alongside other proxy servers
i installed stalwart and ferron on same machine, ferron take over http(80) and https(443) ports, stalwart take over other ports on the machine
i set stalwart http listener on random port and set ferron to send requests on that port, and i disabled stalwart https listener
i’m not sure if i am doing things right or if i am missing something

Nothing ferron-specific is needed. Plain HTTP upstream with the HTTPS listener disabled is the layout we recommend, and Overview | Stalwart describes it in terms that apply to any proxy in front.

Set STALWART_PUBLIC_URL to the URL your clients type at ferron. Discovery documents build every absolute URL from that variable, and with it unset they advertise https://<defaultHostname> on 443, which ferron is holding. That is the usual reason sign-in fails behind a proxy while everything else looks healthy.

Then enable useXForwarded on the Http object so SPF, rate limits and auto-ban see the real client. Bind the HTTP listener to loopback or a private interface first, because that flag trusts the header from any source that can reach the listener. If ferron speaks Proxy Protocol, that plus proxyTrustedNetworks is the stricter option; do not enable both on one listener. This is worth getting right: if Stalwart sees ferron as the source, one malformed request can scan-ban it and lock out everybody behind it at once.

SMTP, IMAP, POP3 and ManageSieve are not HTTP and cannot pass through ferron, so those stay pointed at Stalwart directly, which is what you have. You will still need a certificate inside Stalwart for the implicit-TLS mail ports, and with ferron on 443 the TLS-ALPN-01 challenge cannot complete, so either move that domain to DNS-01 through automatic DNS management or import ferron’s certificate into a Certificate object. A ferron page is a fair ask and I have noted it.

thanks to explanations

ferron help to use it’s certificates for other services

ferron also support proxy protocol

when i try to use proxy protocol i get broken connection

just to be clear when i try to config proxy the connection from proxy server to stalwart should be ‘http ://localhost’ and not ‘https ://localhost’, right? (i used space after protocol so bot won’t take it as link, there is a limit to number of links)

and what value should i set for proxyTrustedNetworks? 127.0.0.0/8 is not working

i can already send and receive email with my local mail server accounts on various clients

i can send email from gmail account to stalwart account, and receive it

but when i send email from stalwart account to gmail account, i get email sent message and email go to sent directory, but i won’t get anything on gmail account

Yes, plain http://127.0.0.1:<port> upstream, no TLS on that hop. The upstream scheme has no bearing on the discovery URLs; those come from STALWART_PUBLIC_URL.

127.0.0.0/8 is very likely the reason Proxy Protocol breaks. If ferron connects to the name localhost it will usually get ::1, and an IPv6 peer does not match an IPv4 CIDR, so Stalwart decides the connection is not from a trusted proxy, skips the header, and hands ferron’s PROXY ... line to the HTTP parser as if it were a request. Add ::1 alongside it, or point ferron at 127.0.0.1 explicitly. A non-empty trusted-network list is itself the switch that turns Proxy Protocol on, incidentally; there is no separate flag. Both v1 and v2 are accepted, so the version is not your problem.

Also move that setting off the global one. proxyTrustedNetworks on SystemSettings applies to every listener, so with 127.0.0.0/8 set globally your SMTP, IMAP and ManageSieve listeners now expect a proxy header from anything on loopback, including stalwart-cli and any local health check. Put it on the HTTP listener ferron talks to via overrideProxyTrustedNetworks and leave the global list empty, and turn useXForwarded off on that listener since it should use one mechanism or the other. If the address matches but the header is malformed you will see network.proxy-error in the log at warn level.

The Gmail one is a separate problem and landing in Sent tells us nothing, since that happens when you submit, before anything is delivered. Search your log for delivery.delivered for that recipient: it is only written after Gmail’s MX answers 250, and it records Gmail’s own reply text. If it is there, the mail left and Gmail is filing or dropping it, which is a PTR and SPF/DKIM/DMARC question. If it is not there, look for delivery.connect-error, which on a new VPS almost always means the host blocks outbound port 25. Which of the two do you see?

the gmail problem seems to be connection issue

when i try delivery tests everything is ok but TCP Connection

i get these errors:

Connection timeout

I/O error: Network is unreachable (os error 101)

pointing directly to the ip of the machine that run proxy server worked

i only set 127.0.0.1 as trusted just for http listener

and currently i don’t have https listener

is there a way (some way to test) to be sure proxy protocol is working as expected?

there is no log with delivery.delivered

i see delivery.connect-error logs

That settles the Gmail half: the mail never left the host. delivery.connect-error with no matching delivery.delivered means we never got a 250 from Gmail’s MX, so ferron and the whole proxy question are out of the picture for that one.

The two errors are two separate failures. “Network is unreachable” is the IPv6 attempt on a host with no IPv6 route, which is the normal state of a Docker bridge network; set ipLookupStrategy to v4Only on the MX route and that noise stops. The connection timeout is the IPv4 attempt, and on a fresh VPS that is nearly always the provider blocking outbound port 25. Try it from the host itself, and if it is blocked you either get them to open it or configure a relay route through something that can send for you.

For Proxy Protocol, look at the RemoteIp recorded on any http.request-url event. Your own browser’s address means the header is being stripped; ferron’s address means it is not. Check that sooner than later, because the failure is quiet and expensive: when the peer is not in the trusted list, that PROXY line goes to the HTTP parser, the parse error counts against the port-scanning threshold, and the address it counts against is ferron’s. Thirty of those in a day and ferron gets blocked with no expiry. Since you moved ferron onto the machine’s IP but the trusted list still says 127.0.0.1, the source address probably no longer matches.

Two corrections to what I told you last time. useXForwarded is not per-listener, it sits on the Http object and applies to every HTTP listener, and it is off by default, so there was nothing there for you to turn off. And overrideProxyTrustedNetworks is read when the listeners start, so it needs a restart of the container; a settings reload will validate it and then quietly keep using the old value. Our configuration docs say otherwise, which is our mistake.

whenever i login to my mail client i have to accepts certificates

and when i try to use lettre lib (for example) i get error about self-signed certs

i am not sure what’s the problem

i configured stalwart to use certs from ferron for email.example.com

“Self-signed” means Stalwart has no usable certificate loaded at all. It only falls back to the certificate it generates at startup when not a single Certificate object loads; a certificate for the wrong name would give a name mismatch error, not this. See what’s being served:

openssl s_client -connect email.example.com:993 -servername email.example.com -showcerts </dev/null

If the subject is rcgen self signed cert, that’s the fallback, and the log should also have tls.no-certificates-available.

My guess is the private key. Stalwart validates only the certificate when you create the object and reads the key later, when certificates load, so a key file ferron keeps at 0600, or a path that isn’t mounted into the Stalwart container, is accepted on save and then skipped. Running the ReloadTlsCertificates action returns the load errors and will show you which it is. Also point certificate at ferron’s full-chain file, and connect clients to email.example.com, not the IP.

Stalwart doesn’t watch those files, so when ferron renews, copy them to a directory the stalwart user owns and run the reload action again. The Caddy page has a systemd unit for exactly this that carries over to ferron unchanged: Caddy | Stalwart

running openssl give me self signed cert

i think stalwart have access and can read ferron cert fiels, in admin client when i check loaded certs i can see cert details

how can i run stalwart actions? you mean in admin panel or the cli?

i added 2 certs in admin panel, one for email.example.com and one for example.com itself

in admin panel stalwart successfully read certs

in logs in admin panel i see:

No TLS certificates available total = 0

id domains section i only have one domain: example.com, cert settings for this domain was set to manual, i changed this to auto and then restart the service but i don’t have certs yet

Revert the domain back to Manual certificate management. Automatic needs an acmeProviderId pointing at an AcmeProvider object, so flipping the switch alone issues nothing, and with ferron holding 80 and 443 the ALPN challenge cannot complete anyway. It also does not import ferron’s certificate, which is what you want.

On the actions question: Management > Actions in the admin UI, or stalwart-cli create action/ReloadTlsCertificates from the shell, then stalwart-cli get action to read the result. That result is the useful part here, because it returns the per-object load errors.

total = 0 in that log line means no Certificate object loaded at all, which is not an SNI or selection problem. Next to it there will be a registry.build-error with Source = certificate naming the actual reason. Post that line verbatim. The two likely ones are “Failed to obtain private key secret” (the key file is not readable by the server, which is common when it is ferron’s root-owned 0600 key) and “Invalid certificate: No private keys found” (an encrypted or DER key, an empty privateKey field, or a certificate pasted into privateKey).

The panel showing issuer and validity dates does not tell you the key is good. We only parse the certificate field when you save the object and fill in the metadata from it; the private key is never read until load time. So a broken key saves cleanly and fails silently later. Only PKCS#8, PKCS#1 and SEC1 PEM keys are accepted, and certificate should point at ferron’s fullchain PEM.

Once certificates do load, set defaultCertificateId on SystemSettings as well, since a client that sends no SNI falls back to it. And the proxy-trust point from earlier still applies: now that ferron reaches Stalwart on the machine IP and not on loopback, that address has to be in overrideProxyTrustedNetworks on the HTTP listener, with a container restart for it to take.