Automatically generate full chain certificate file from CA

1. The problem I’m having:

I have my own local root CA trusted on every device. I generated an intermediate CA which I have given to Caddy to make local certificates. It works fine for HTTP, since Caddy seems to serve the full chain certificate to the browser. But, I would like to use Caddy as a CA to generate certificates for my mail server (imap.ldb and smtp.ldb). The certificate generated in /var/lib/caddy/.local/share/caddy/certificates/ldb/imap.ldb/imap.ldb.crt seems to only be the server certificate, without the intermediate included. I know that I can just cat them together to make a full chain file, but I would like to do this automatically because the default expiry is short (12 hours). Using a cron job would risk having long downtimes in between when the certificate is renewed, and when the job runs next.

2. Error messages and/or full log output:

none

3. Caddy version:

v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=

4. How I installed and ran Caddy:

Installed using Debian package (from Cloudsmith repo listed in Caddy docs), running using the included systemd unit.

a. System environment:

Debian 12, Caddy is running bare-metal

b. Command:

systemctl enable --now caddy

c. Service/unit/compose file:

Literally the included one.

# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateDevices=yes
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

{
	skip_install_trust
	pki {
		ca ldb {
			intermediate {
				cert /var/lib/caddy/srv02-caddy.cert.pem
				key /var/lib/caddy/srv02-caddy.key.pem
			}
		}
	}
	cert_issuer internal {
		ca ldb
	}
}

imap.ldb {
	respond "imap.ldb"
}
smtp.ldb {
	respond "smtp.ldb"
}

5. Links to relevant resources:

You can use GitHub - mholt/caddy-events-exec: Run commands on Caddy events to configure the cert_obtained event to copy the certificate and combine them yourself with a shell script immediately after it’s renewed. You’ll probably need to use v2.7.0-beta.2 to get the latest updates to the event payload. See https://github.com/caddyserver/certmagic/blob/25bb2afb2c082163ecc46a72df23b703c56fb5ef/config.go#L615 for the payload data, can be used in the command string with {event.data.storage_path} for example.

3 Likes

That worked, but in the middle as I was writing my script, I realized that the certificate from Caddy directory actually is the chain with intermediate, but Thunderbird uses its own trust store so my root CA wasn’t added, causing the original error. Oops.

2 Likes

It turns out Dovecot annoyingly doesn’t pick up the certificate change after it’s renewed by Caddy, so I did have to set this back up to reload Dovecot automatically. I made a script /var/lib/caddy/reload-dovecot.sh (with owner root:caddy and permissions 750, and ./mg is a script I have to run the docker command more easily):

#!/bin/sh

cd /srv/mail
./mg reload dovecot

Added to sudoers:

caddy ALL=(root) NOPASSWD: /var/lib/caddy/reload-dovecot.sh

Added to my global options in Caddy:

events {
	on cert_obtained exec sh -c "sudo /var/lib/caddy/reload-dovecot.sh"
}
1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.