Using Caddy's automatically generated certificates for other services

Hello all!

1. The problem I’m having:

I am using Caddy as a reverse proxy engine, and making use of Caddy’s functionality to automatically generate and maintain certs. One of the services that Caddy is proxying to also needs a cert for its own functionality, so I need to re-use the generated cert. What is the standard/best practice way of accomplishing this? Since Caddy can potentially use one of multiple services to generate certs, the exact folder the cert is in may change at any given time. I imagine one could devise a rube goldberg script that takes this into account and figures out the newest available script, copies stuff around, etc. But wanted to check if there is a simple and supported method using the official Caddy binary.

3. Caddy version:

v2.11.1 h1:C7sQpsFOC5CH+31KqJc7EoOf8mXrOEkFyYd6GpIqm/s=

4. How I installed and ran Caddy:

The official install instructions provided for Debian systems

a. System environment:

Standard Debian 12 environment

You can use events to run scripts in reaction to cert maintenance Global options (Caddyfile) — Caddy Documentation

1 Like

I did come across this document, but for this usecase (certificate generated) it appears to depend on an unofficial plugin which isn’t thoroughly tested: “For example, to run a command after a certificate is obtained (third-party plugin required)”.

The plugin it links to: GitHub - mholt/caddy-events-exec: Run commands on Caddy events has this warning:

It is EXPERIMENTAL and subject to change. After getting some production experience, if demand is high enough and if this is generally useful to most users, we may move this into the standard Caddy distribution. It would be the only Caddy module that executes commands on the system, so we want to make sure it does not allow for arbitrary commands to be executed.

Which gave me pause. I figured there is probably a way people are doing this on production servers, if it’s not this way.

Well, FWIW, that’s how I would do it :joy:

It’s possible that I am worrying too much over that big warning!

Experimental just means “the API/interfaces might change based on user feedback, so be aware you may need to react and adjust your config at some point”. It’s entirely usable as-is though.

One other reason it’s a plugin is because there’s a certain amount of danger to be able to run arbitrary commands defined in config if someone finds a way to change config at runtime (extremely unlikely, but it would allow escalating a vulnerability via arbitrary command execution, if there was one).

1 Like

Thanks folks, I will take another look at the module way of doing this.

For reference, I am using Caddy to proxy HTTP/JMAP for a Stalwart instance while Stalwart listens directly on SMTP and IMAP. Stalwart needs a cert for IMAP and SMTPS, of course, so what I do is this:

  • Caddy orders the cert.
  • Caddy saves it to <caddyroot>/certificates/acme-v02.api.letsencrypt.org-directory/<domain>/<domain>.crt, caddyroot being /var/lib/caddy/ by default.
  • A Systemd path unit listens on changes to that certificate.
  • Upon change of the cert, it and the key are put to where Stalwart expects it to find.
  • Stalwart CLI is used reload the certificate.
# /etc/systemd/system/stalwart-cert-import.path
[Unit]
Description=Stalwart certificate import from Caddy

[Path]
PathModified=/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<domain>/<domain>.crt

[Install]
WantedBy=multi-user.target

# /etc/systemd/system/stalwart-cert-import.service
[Unit]
Description=Stalwart certificate import from Caddy

[Service]
Type=oneshot
ExecStart=/usr/bin/install -p -m644 -o stalwart-mail -g stalwart-mail /var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<domain>/<domain>.crt /var/lib/stalwart-mail/cert/<domain>.crt
ExecStart=/usr/bin/install -p -m640 -o stalwart-mail -g stalwart-mail /var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<domain>/<domain>.key /var/lib/stalwart-mail/cert/<domain>.key
ExecStart=/usr/bin/stalwart-cli -u https://<domain> -c user:pass server reload-certificates
2 Likes

Thanks for the context and suggestion! As a note, my understanding is that, while Caddy defaults to Lets Encrypt, it will fall back to other providers such as ZeroSSL if it fails to renew/generate a cert from LE. In such a case, the new cert would be placed in a different, relevantly named folder. The chances of this happening are probably low, but nonetheless present, which is what gave me pause on just checking a single folder. I’m guessing there might be other cases and reasons the exact folder name would change as well, whether due to in-prod scenarios, or version/API changes on Caddy’s end or one of the underlying tools, such as ACME. So a truly robust solution would need to do some fancy magic where it iterates through the whole “certificates” folder and deduces which cert is the newest/correct one.

I was ready to do this if needed, but since I already took the plunge into using the events exec module, I’ll be sticking with that for now. Just wanted to explain the reason I didn’t initially go with the solution you showed here, in case my reasoning is useful to you as well!

You concern is addressed by explicitly specifying Let’s Encrypt Live as ca. Also be advised that any ACME client will attempt renewal way ahead of the expiration date, so even small downtimes of Let’s Encrypt are not a problem.

If you insist on the fallback, though, my suggestion would be to create another set of path/service units and to use conditionals: ConditionPathExists or ConditionDirectoryNotEmpty, like so:

ConditionDirectoryNotEmpty=/var/lib/caddy/acme-v02.api.letsencrypt.org-directory/<domain>

and

ConditionDirectoryNotEmpty=/var/lib/caddy/acme.zerossl.com-v2-DV90/<domain>

This for reference for future readers. :slight_smile: