I would like to run https for both Stalwart’s WebUI and a Webmail app.
Stalwart is using the default 443 listener, which can be disabled. This way one can run a proxy and terminate the connection to :8080.
As expected, the proxy is running on 443. As a result, Stalwart is not able to create/renew the SSL certificate via LetsEnecrypt.
I’ve setup CloudFlare DNS and the necessary DNS entries are created, but the practical certificate is issued by LetsEncrypt, which needs 443.
Also, I believe that the created certificate is used for pop3/imap/smtp so it is crucial that Stalwart continues to be able to create and renew certificates. Which, as a result, means that 443 needs to be open and managed by Stalwart, which means that a proxy cannot be installed on the same machine.
Except running the Webmail app on another machine, what would be the necessary workaround?
Thank you
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.
This gives exactly the answer: use DNS-01 (Cloudflare API integration) instead of HTTP-01/TLS-ALPN-01, so port 443 doesn’t need to belong to Stalwart at all.
This confirms the answer: Stalwart supports DNS-01 challenges, which don’t need port 443/80 open at all, and can use Cloudflare directly as a DNS provider.
I have enough to write a full answer now.
Use the DNS-01 challenge instead of TLS-ALPN-01/HTTP-01. Since you already have Cloudflare DNS set up, this is exactly the workaround: DNS-01 validates domain ownership by publishing a TXT record, so it never needs port 80 or 443 to be open or reachable on Stalwart at all. That means Stalwart can keep issuing/renewing its own certificate (used for IMAP/POP3/SMTP/HTTPS) while your reverse proxy owns port 443 exclusively for HTTP(S) traffic to both the WebUI and your webmail app.
To set it up:
Register a DnsServer object of type Cloudflare with an API token that can manage the relevant zone (Settings → DNS Providers).
Register (or edit) your AcmeProvider object and set challengeType to Dns01 (or DnsPersist01 if you want Stalwart to avoid re-publishing a TXT record on every renewal).
On the Domain object, set dnsManagement to Automatic and point dnsServerId at the Cloudflare DnsServer, and set certificateManagement to Automatic pointing at the AcmeProvider.
With this, Stalwart never needs port 80/443 opened for ACME, so you can freely run your reverse proxy (terminating both the WebUI and webmail app on 443) on the same machine, and route Stalwart’s admin UI to its internal port (e.g. 8080) behind the proxy as usual.
Reference: ACME Configuration — see docs/server/tls/acme/configuration.md and docs/server/tls/acme/challenges.md (DNS-01 section), plus DNS providers (docs/server/dns/provider) for the Cloudflare DnsServer setup.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.
The best solution would be to not use a reverse proxy. You can bind Stalwart so it only listens on x IP address. Meaning you can still use other IP addresses for your main web server.
Yeah, it’s a bit annoying to run the Web mailer on the same IP address as Stalwart. So running on separate addresses makes your life a bit easier.
If you do want to try running the Web mail frontend on the same address as stalwart, you’ll need a reverse proxy. There is some documentation but it is incomplete. A bit more complete approach to using Caddy to sit in front of Stalwart is here: GitHub - muelli/caddy_fronting_stalwart: Ansible role for managing a Caddyfile and Stalwart configuration · GitHub Once that setup works, you can relatively simply add another (sub)domain to forward to, say, Bulwarkmail.
I had issues when both Caddy and Stalwart were trying to obtain certificates. I hope you won’t run into similar issues with your setup.
Because of the problems I encountered, I have Caddy be responsible for certificates and make Stalwart aware of their existence.
It’s actually really simple: Nginx Proxy is the key, and Nginx manages the Let’s Encrypt certificates using Certbot. A deployment script copies the certificates to a specified location after each renewal, sets the permissions, and then restarts Stalwart. In the Stalwart admin panel, under “Manage TLS certificates,” you simply create an entry for the domain(s) using “Read from file,” and it does exactly what it’s supposed to.
Deploy-Script:
#!/bin/bash
set -e
# Pfad zu deiner Domain (hier deine echte Subdomain eintragen!)
DOMAIN="mail.domain.com"
TARGET_DIR="/etc/stalwart/certs"
# 1. Zertifikate in den Stalwart-Ordner kopieren
cp -f /etc/letsencrypt/live/$DOMAIN/fullchain.pem $TARGET_DIR/fullchain.pem
cp -f /etc/letsencrypt/live/$DOMAIN/privkey.pem $TARGET_DIR/privkey.pem
# 2. Rechte an den Stalwart-Systembenutzer bergeben
chown -R stalwart:stalwart $TARGET_DIR
chmod 600 $TARGET_DIR/privkey.pem
chmod 644 $TARGET_DIR/fullchain.pem
# 3. Stalwart anweisen, die Zertifikate live neu zu laden
systemctl restart stalwart
If you want to use Bulwart, Linus has even posted an Nginx config somewhere that includes only the relevant Stalwart routes and thus secures everything relevant to the outside world.
I recently put a full mail stack and a webmail front-end on a single host, both sitting behind one Traefik instance and sharing port 443. The interesting part was letting Stalwart terminate its own TLS while Bulwark Mail lets Traefik do the termination — on the same entrypoint, without a port conflict.
The problem
A mail server like Stalwart really wants to own its TLS. It speaks SMTP, IMAP, and JMAP over the same certificate, does its own STARTTLS handling, and can manage its own ACME renewals. Handing that to a reverse proxy means re-encrypting everything and losing protocol-level control.
The webmail app is the opposite case: it’s plain HTTP internally, so letting the proxy terminate TLS and forward cleartext to the container is simpler and gives you the proxy’s middlewares for free.
Two different needs, one :443.
The solution: SNI-based split
Traefik decides what to do before terminating, based on the SNI in the TLS handshake:
Stalwart → a TCP router with HostSNI(<mail-host>) and tls.passthrough = true. Traefik reads only the SNI and hands the still-encrypted stream straight to the container. Stalwart terminates.
Bulwark Mail → a normal HTTP router with TLS termination. Traefik decrypts and forwards cleartext to the container.
These coexist because TCP routers are evaluated before HTTP routers, and a specific HostSNI beats the HTTP catch-all. No priorities to tune.
Rough shape of the container labels (placeholders throughout):
Because Traefik never decrypts Stalwart’s traffic, Stalwart can’t rely on an HTTP or TLS-ALPN challenge reaching it through the proxy. So Stalwart gets its certificate via ACME DNS-01 against the DNS provider directly — no inbound :80/:443 needed for validation at all. That pairs perfectly with passthrough: the proxy just moves bytes, and the mail server quietly renews its own cert out of band.
On my end I’ve used the DNS-01 solution and worked without any issues. I believe that it’s safer to allow both services (Stalwart via DNS-01 and NGINX via Certbot) to renew independently and as designed.