Keeps Stalwart Mail Server's TLS certs (and optionally its DANE/TLSA record in Cloudflare) in sync with certs issued by Caddy.
The set of scripts consists of:
stal-lib-cert-copy.sh- shared library, sourced by the other two scripts (not run directly)stal-copy-caddy-certs.sh- manually copy a Caddy cert+key into Stalwart's certs dirstal-tlsa-update.sh- unattended (cron/systemd): copies the cert, restarts Stalwart, updates the Cloudflare TLSA record if the cert changedinstall.sh- installs all scripts and (optionally) the systemd timer
All scripts must stay in the same directory.
sudo apt update && sudo apt install openssl dnsutils curl jqDocker is assumed to be installed already, since that's how Stalwart runs.
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/stackfusion/stalwart-caddy-certs/HEAD/install.sh)"Or, from a local clone:
sudo ./install.shEither way, this copies the scripts to /usr/local/lib/stalwart-caddy-certs/, symlinks them into /usr/local/bin/, and (interactively) offers to install + enable the systemd timer (runs every 5 minutes). For non-interactive installs, set SETUP_TIMER=y to enable the timer automatically:
sudo SETUP_TIMER=y bash -c "$(curl -fsSL https://raw.githubusercontent.com/stackfusion/stalwart-caddy-certs/HEAD/install.sh)"You'll still need to fill in your Cloudflare credentials in /etc/stal-tlsa-update.env afterward — see below.
sudo stal-copy-caddy-certs.sh -d mx.example.com -s ./stalwart-etc/certs/Copies the cert/key with correct ownership (uid/gid 2000:2000, mode 640) and atomic rename, so Stalwart never sees a partial write.
On each run: compares the local cert's public-key hash to Cloudflare's published TLSA record. If unchanged, exits. If changed: copies the cert into place, restarts Stalwart, and updates the TLSA record (DANE-EE, 3 1 1).
Warns if the Caddy cert is missing the intermediate (leaf-only), which can break DANE validation for clients without Let's Encrypt's intermediate cached.
Uses flock (/var/lock/stal-tlsa-update.lock) so only one run happens at a time.
All variables below have defaults and can be overridden via environment:
| Variable | Default |
|---|---|
DOMAIN |
example.com |
SUBDOMAIN |
mx |
PORT |
25 |
CADDY_CERT_PATH |
derived from DOMAIN/SUBDOMAIN |
STALWART_CERT_DIR |
/opt/stalwart/certs |
STALWART_CONTAINER |
stalwart |
CLOUDFLARE_NAMESERVER |
@anastasia.ns.cloudflare.com |
LOCK_FILE |
/var/lock/stal-tlsa-update.lock |
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are required, with no default.
-
Create a token at https://dash.cloudflare.com/profile/api-tokens with
Zone → DNS → Editfor your zone (optionally restrict by server IP). -
Get your Zone ID:
curl https://api.cloudflare.com/client/v4/zones \ -H "Authorization: Bearer YOUR_API_TOKEN" | jq -r '.result[] | {name, id}'
-
Store both in a root-only env file:
sudo install -m 600 -o root -g root /dev/null /etc/stal-tlsa-update.env sudo tee /etc/stal-tlsa-update.env >/dev/null <<'ENV' CLOUDFLARE_API_TOKEN=your-token-here CLOUDFLARE_ZONE_ID=your-zone-id-here ENV
For a one-off manual run instead:
export CLOUDFLARE_API_TOKEN="your-token-here"
export CLOUDFLARE_ZONE_ID="your-zone-id-here"
sudo -E stal-tlsa-update.shCaution
If a real token ends up in a chat, doc, ticket, or repo, roll it immediately at https://dash.cloudflare.com/profile/api-tokens (⋯ → Roll/Delete), then update /etc/stal-tlsa-update.env.
install.sh can set this up for you. Manually, it's:
/etc/systemd/system/stal-tlsa-update.service- loads/etc/stal-tlsa-update.env, runsstal-tlsa-update.sh/etc/systemd/system/stal-tlsa-update.timer- runs every 5 minutes, 2 min after boot
sudo systemctl daemon-reload
sudo systemctl enable --now stal-tlsa-update.timer
sudo systemctl status stal-tlsa-update.timer
sudo journalctl -u stal-tlsa-update.service -f*/5 * * * * . /etc/stal-tlsa-update.env && /usr/local/bin/stal-tlsa-update.sh >> /var/log/stal-tlsa-update.log 2>&1Full chain check (should print 2):
grep -c "BEGIN CERTIFICATE" /path/to/caddy/cert.crtVerify Cloudflare token:
curl https://api.cloudflare.com/client/v4/user/tokens/verify \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"Current TLSA record:
dig +short TLSA _25._tcp.mail.example.orgQuery Cloudflare's authoritative DNS directly:
dig +short @nova.ns.cloudflare.com TLSA _25._tcp.mail.example.orgCheck outbound IP version:
curl -4 https://api.cloudflare.com/cdn-cgi/trace
curl -6 https://api.cloudflare.com/cdn-cgi/trace