Skip to content

Caddy

Caddy is a web server and reverse proxy that obtains and renews its certificates automatically. In front of Stalwart it is typically used when the host already runs Caddy for websites on port 443, and Stalwart has to share that port.

This page shows a configuration with a division of labour between the two. Caddy handles HTTPS for the mail host name. It forwards the mail ports unchanged to Stalwart, which terminates TLS for them, using certificates copied from Caddy.

When clients reach the server under a URL other than https://<hostname>, set STALWART_PUBLIC_URL to that URL. Public URL explains why.

Caddy forwards only HTTP by itself, and the mail protocols (SMTP, IMAP, POP3, ManageSieve) are not HTTP. Forwarding the mail ports therefore requires a Caddy binary built with the community-maintained caddy-l4 plugin, which adds TCP forwarding and the PROXY protocol. The following xcaddy command builds such a binary:

xcaddy build --with github.com/mholt/caddy-l4

Where a custom build is not wanted, forward the mail ports with HAProxy or NGINX instead, as described on their pages, and use Caddy for HTTP only.

Caddy binds the public ports on this host, and two programs cannot bind the same port. Stalwart’s default listeners for those ports must therefore be removed or moved to other addresses.

The following Caddyfile does two things. It forwards the mail ports to the backend listeners on 127.0.0.1, with PROXY protocol version 2. It also proxies HTTPS for mail.example.com to the backend HTTPS listener:

{
layer4 {
0.0.0.0:25 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10025
}
}
}
0.0.0.0:465 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10465
}
}
}
0.0.0.0:993 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10993
}
}
}
0.0.0.0:995 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10995
}
}
}
0.0.0.0:4190 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:14190
}
}
}
}
}
mail.example.com {
reverse_proxy https://127.0.0.1:10443 {
transport http {
proxy_protocol v2
tls_server_name mail.example.com
}
}
}

The two kinds of traffic take different paths. The mail connections reach Stalwart unchanged, so Stalwart performs the TLS handshake for them. HTTPS is decrypted by Caddy and encrypted again towards the backend listener, and tls_server_name makes Caddy request the certificate for mail.example.com from Stalwart.

Caddy sends the PROXY header without the SSL extension. That is correct here, because Stalwart sees the TLS connections itself.

This setup affects how Stalwart gets its certificates. Caddy occupies port 443, so the TLS-ALPN-01 challenge of Stalwart’s own ACME client cannot reach Stalwart. There are two ways around this: let Stalwart obtain its certificates with the DNS-01 challenge, or copy Caddy’s certificates as described below.

Caddy already obtains a certificate for the mail host name, so Stalwart can use the same one. Caddy stores the certificates it obtains under its data directory, in certificates/<issuer>/<host>/<host>.crt, with the private key in <host>.key. For the Caddy service on Linux, the data directory is usually /var/lib/caddy/.local/share/caddy.

A Stalwart Certificate (found in the WebUI under Settings › TLS › Certificates) can read copies of these files:

{
"certificate": {"@type": "File", "filePath": "/etc/stalwart/tls/mail.example.com.crt"},
"privateKey": {"@type": "File", "filePath": "/etc/stalwart/tls/mail.example.com.key"}
}

Stalwart reads the files only when it loads its certificates. After each renewal by Caddy, the files must therefore be copied again, and the certificates reloaded with the ReloadTlsCertificates action, as described in Certificates.

A systemd path unit can do this automatically, whenever Caddy writes a new certificate. It consists of two units: a path unit that watches the certificate file, and a service unit that copies the files and reloads the certificates.

stalwart-certs.path:

[Unit]
Description=Copy Caddy certificates to Stalwart
[Path]
PathModified=/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt
[Install]
WantedBy=multi-user.target

stalwart-certs.service:

[Unit]
Description=Copy Caddy certificates to Stalwart
[Service]
Type=oneshot
ExecStart=/usr/bin/install -o stalwart -g stalwart -m 0640 /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt /etc/stalwart/tls/
ExecStart=/usr/bin/install -o stalwart -g stalwart -m 0600 /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key /etc/stalwart/tls/
ExecStart=/usr/local/bin/stalwart-cli --url https://mail.example.com --api-key <token> create Action/ReloadTlsCertificates

Enable the path unit with systemctl enable --now stalwart-certs.path. Without systemd, a daily cron job that runs the same three commands has the same effect, with a delay of up to a day after a renewal.

Clients that send no server name receive the default certificate. To have them receive this certificate, set SystemSettings.defaultCertificateId to it.