Skip to content

Certificates & HTTPS

Caddy obtains and renews certificates on its own. CPM’s job is to tell it what to obtain, to show you what exists, and to hold the credentials that DNS challenges need.

The certificates listDemoNothing you change here is saved
ACME
1
1 on enabled hosts
Imported
21 expired
At least one has already lapsed
CA / mTLS
1
4 client certificates issued
Roles
2
Grant paths to client certificates
DomainIssuerChallengeExpiresStatus
app.example.comLet's EncryptHTTP-01in 61 days
Valid
legacy.example.comDigiCertImported PEMin 9 days
Expiring soon
vpn.example.comInternal CA-expired
Expired

Every proxy host gets HTTPS by default through ACME - Let’s Encrypt, or any directory you point it at. Caddy obtains and renews these on the agent, and the Certificates page reads each agent’s certificate storage to show when every host’s certificate expires and who issued it. An agent older than this feature leaves the column empty rather than guessing.

Each host’s menu has three tools:

  • Renew now. Caddy has no renew button of its own, so CPM puts the host’s names under a renewal window wide enough to include their current certificates and reloads twice: once without them, so Caddy forgets the certificates it has cached, and once with them, so it reads them back and renews. HTTPS to those names fails for the moment between the two reloads; after that Caddy serves the old certificate until the new one arrives, so a failed renewal doesn’t take the site down. The names go back to their normal schedule once the new certificates appear, or after 15 minutes if none do.
  • Test reachability. Resolves each domain, lists its CAA records, and fetches a probe path over plain HTTP to see whether the request lands on this Caddy - the same trip an HTTP-01 challenge makes. It only ever fetches the host’s own domains. From there you can also ask Let’s Debug to check from outside; that sends the domain name to a third party, so it runs only when you click it.
  • Download. The certificate, or its private key. The key needs a sign-in from the last ten minutes, and every key download is written to the audit log. Imported certificates have the same two downloads.
Certificates → ACMERenew one, or test its reachability, from its menu. Downloads need a controller.DemoNothing you change here is saved
Row statusProxy HostExpiresStatusActions
App
app.example.com
…
Active
Grafana
grafana.example.com +1
…
Active
Status page
status.example.com
…
Active
Staging
staging.example.com
…
Paused

For an internal CA, set the ACME directory URL under Settings → General → ACME Server and supply its root so Caddy trusts it.

HTTP-01 needs port 80 reachable from the internet. DNS-01 does not, which is what makes wildcard certificates and internal-only hosts possible.

Twenty-two providers are supported, including Cloudflare, Route 53, DigitalOcean, Hetzner, Vultr, Porkbun, GoDaddy, Namecheap, OVH, IONOS, Linode, netcup, deSEC, Dynu, acme-dns, INWX, ClouDNS and RFC2136 (BIND/TSIG). Credentials are encrypted at rest, and propagation delay and timeout are configurable per provider

  • netcup ships with slow-propagation defaults because it needs them.

A provider can be overridden per certificate when one domain lives somewhere else - through the REST API only (providerOptions.provider); the dashboard uses the global provider. The override needs that provider’s credentials saved in Settings, or the global provider is used.

A domain whose DNS host has no API can still use DNS-01. Create a CNAME from its _acme-challenge name to a record in a zone you can write through a supported provider, and add a delegation under Settings → DNS → DNS Providers → Challenge delegation:

  • Domain - covers itself and every name under it; the longest matching domain wins. A wildcard *.example.com answers at _acme-challenge.example.com, so it shares the apex’s record. Every other name needs its own CNAME, all pointing at the same target.
  • CNAME target - where the challenge record is written. Caddy does not follow the CNAME itself, so CPM gives it the target directly (override_domain) and puts delegated names in an automation policy of their own.
  • Provider - optional; writes the record at the target. It beats a certificate’s own provider, because it is the one that can write that zone. Without one, the certificate’s or the default provider is used.

The table looks each CNAME up from the controller and warns when one is missing or points elsewhere. It is a warning, not a refusal: the controller’s resolver may not see what the certificate authority sees. A delegated name whose provider is missing or not compiled into Caddy falls back to HTTP-01, and the controller log says so.

For a self-hosted BIND server, you do not need delegation - use the RFC2136 provider with a TSIG key.

acme-dns is a tiny DNS server that holds only challenge records, so the credential CPM keeps can change nothing else.

  1. Run acme-dns somewhere the certificate authority can query it, or pick a server you trust.
  2. Under Register with acme-dns, enter the domain and the server’s URL, and click Register. The controller creates an account there, stores its password encrypted, and adds a delegation for the domain.
  3. Create the one record it shows, _acme-challenge.<domain> CNAME <account>.<server>, at your DNS host, and apply the change.

Each domain gets its own account, because one account holds only the two newest challenge records - enough for a name and its wildcard, not for more. A name under a registered domain uses that domain’s account; its own CNAME points at the same target. The server URL must be HTTPS unless the server is on a private address or a single-label name such as a Compose service. The public auth.acme-dns.io is only a placeholder - CPM never registers anywhere you did not type.

Some tools let you type the challenge TXT record by hand. Caddy has no such solver, and a record typed by hand would have to be typed again at every renewal, every 60 to 90 days, by someone who is awake when it runs. A CNAME is created once. Delegation covers every case the manual mode would.

Bring your own certificate and key. A pair is refused on save if the certificate is not PEM X.509, the key is not an unencrypted PEM key, or the key does not match the certificate - Caddy would refuse to load it, and every host would stop updating with it. Expiry is tracked, and the page counts down to the next import to lapse, so one that is about to expire is visible rather than a surprise. With email set up, the administrators are also told by mail - about an imported certificate nearing expiry, and about a managed one Caddy has failed to renew.

A certificate a host still uses cannot be deleted - the dialog names the hosts, and the REST API answers 409. Choose another certificate on them first, so no host quietly falls back to automatic issuance. Delete unused removes every imported certificate in the list that no host uses, after naming them; usage is checked again as it runs.

A certificate something else keeps renewing on an agent’s host - certbot, acme.sh, a corporate tool dropping files on a share - can be read from its files instead of pasted in again every 90 days. Give the agent one directory to read, as the Docker daemon on that host sees it:

# docker-compose.yml, or the agent's own environment on a remote host
agent:
environment:
CERT_FILES_HOST_DIR: /etc/letsencrypt

Restart the agent, then choose Certificates → Import → From a file on an agent. Pick the agent, and the dialog lists the certificates in that directory with their names and expiry; for certbot’s live/<name>/fullchain.pem it picks the matching privkey.pem itself. The certificate is read once before it is saved, so a wrong path or a key that does not match is refused there and then.

From then on the agent reads the files every six hours, when it starts, and when you choose Re-read now in the row’s menu. A renewed certificate is loaded into that agent’s Caddy; an unchanged one changes nothing. The domains come from the certificate’s own names, and a change to them is recorded in the audit log. If a read fails - a file gone, a key that no longer matches - the row says why and the last good certificate keeps serving.

  • Caddy is never pointed at the files. Caddy caches a certificate it loaded from a file and never notices a renewal, and a missing file fails the whole configuration. So the agent reads the files and sends the PEM to the controller, which checks it and hands it to Caddy inline, as it does a pasted certificate.
  • The key travels to the controller, over the agent’s signed connection, and is stored encrypted like a pasted key. Use this for certificates you would be willing to paste in.
  • One agent serves it. The certificate goes only into its own agent’s configuration, so only a host pinned to that agent alone can use it; the editor and the bulk action refuse anything else.
  • Only that directory. The controller names paths inside it and nothing else: no .., no absolute path. The agent reads in a throwaway container with no network that mounts only this directory, read-only, so a symbolic link cannot reach anything else on the host - certbot’s live/ links into its archive/ still work, as both are inside the directory.
  • Permissions. That container reads as root, because certbot keeps archive/ readable only by root; nothing needs changing on the host. Files over 1 MiB and encrypted keys are refused.

CPM can act as its own certificate authority for client certificates - see access lists & mTLS. Issued certificates are tracked and revocable, and revocation is fail-closed: revoking every certificate rejects every connection rather than falling open.