Skip to main content
Version: 0.16

Caddy

Caddy is an open-source, HTTP/2-enabled web server with automatic HTTPS. It covers static file serving, simple reverse proxies, and TLS management out of the box, and is often chosen for the conciseness of its Caddyfile syntax.

Stalwart can sit behind Caddy for HTTP traffic. Caddy handles HTTPS termination and certificate management, then forwards requests to Stalwart.

The example below forwards to Stalwart's HTTPS listener with Proxy Protocol v2, but a plain HTTP-upstream pattern (Caddy forwarding plain HTTP to Stalwart's port 8080) works equally well and produces the same OAuth, OIDC, and JMAP discovery responses. See How discovery URLs are composed for the model that drives those responses, and set the STALWART_PUBLIC_URL variable to the URL clients reach Caddy on whenever it differs from https://<defaultHostname> on port 443.

Note on Layer 4 support

Caddy does not natively forward raw TCP streams, so it cannot forward the mail ports (SMTP, IMAP, POP3, ManageSieve) or preserve the Proxy Protocol on them. To carry client IPs into Stalwart's mail listeners, either put HAProxy or NGINX in front of Caddy, or build a Caddy binary that includes the community-maintained caddy-l4 plugin. The plugin adds layer-4 forwarding and Proxy Protocol emission to Caddy.

For example, using xcaddy:

xcaddy build --with github.com/mholt/caddy-l4/modules/l4proxy \
--with github.com/mholt/caddy-l4/modules/l4tls \
--with github.com/mholt/caddy-l4/modules/l4proxyprotocol

Configuration

The following Caddyfile forwards the mail ports (25, 465, 993, 4190) using caddy-l4 with Proxy Protocol v2 and reverse-proxies HTTPS for mail.example.com:

{
layer4 {
0.0.0.0:25 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10025
}
}
}

0.0.0.0:993 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10993
}
}
}

0.0.0.0:465 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:10465
}
}
}

0.0.0.0:4190 {
route {
proxy {
proxy_protocol v2
upstream 127.0.0.1:14190
}
}
}
}
}

example.com {
redir https://www.example.com{uri}
}

www.example.com {
root * /var/www/imkerei

file_server
}

mail.example.com {
reverse_proxy https://127.0.0.1:10443 {
transport http {
proxy_protocol v2
tls_server_name mail.example.com
}
}
}

Certificate management

Crontab

Automate copying Caddy-issued certificates into Stalwart's certificate directory from cron:

0 3 * * * cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.crt > /opt/stalwart/cert/example.com.pem
0 3 * * * cat /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.key > /opt/stalwart/cert/example.com.priv.pem

Systemd

As an alternative to cron, a systemd path unit can watch the Caddy certificate file, copy the renewed files into place, and trigger a hot reload on Stalwart:

stalwart.path:

[Unit]
Description=import certs from caddy 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.service:

[Unit]
Description=imports certs from caddy to stalwart

[Service]
Type=oneshot
ExecStart=/usr/bin/cp -f /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.pem /opt/stalwart/cert/
ExecStart=/usr/bin/cp -f /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.priv.pem /opt/stalwart/cert/
ExecStart=/usr/bin/curl -X GET -H "Accept: application/json" -H "Authorization: Bearer <TOKEN>" https://mail.example.com/api/reload/certificate

[Install]
WantedBy=multi-user.target

Stalwart configuration

Create a Certificate object (found in the WebUI under Settings › TLS › Certificates) pointing at the copied certificate and private-key files, and reference it from defaultCertificateId on the SystemSettings singleton (found in the WebUI under Settings › Network › Services, Settings › Network › General) so that it is presented when clients do not send SNI. The Certificate fields look like:

{
"certificate": {
"@type": "File",
"filePath": "/opt/stalwart/cert/example.com.pem"
},
"privateKey": {
"@type": "File",
"filePath": "/opt/stalwart/cert/example.com.priv.pem"
}
}