A Caddy module that caches get_certificate results in-process, so
externally-provided TLS certificates are served from memory instead of being re-fetched over HTTP
on every handshake.
Caddy's certificate manager library, certmagic, does
not cache the result of a get_certificate manager: by design it calls the manager on every
TLS handshake (see caddyserver/caddy#7038). For
certificates fetched from an HTTP endpoint via the stock
tls.get_certificate.http getter,
that means a backend round trip on every handshake, even though the certificate rarely changes.
This module is a drop-in alternative that wraps the same HTTP fetch with an in-process cache. The hot path becomes a map lookup (microseconds) instead of an HTTP request.
tls.get_certificate.cached_http (implements certmagic.Manager).
Build Caddy with this module using xcaddy:
xcaddy build --with github.com/ohdearapp/caddy-get-certificate-cacheTo build against a local checkout (for development), point the module at it with a replace:
git clone https://github.com/ohdearapp/caddy-get-certificate-cache.git
xcaddy build --with github.com/ohdearapp/caddy-get-certificate-cache=./caddy-get-certificate-cacheBecause the module is compiled into Caddy, upgrading Caddy means re-running
xcaddywith the module included.
tls {
get_certificate cached_http https://app.example.com/cert {
ttl 1h
negative_ttl 60s
cache_dir /var/cache/caddy-certs
}
on_demand
}| Option | Type | Default | Description |
|---|---|---|---|
| (argument) | URL | — (required) | Endpoint queried on a cache miss. server_name, signature_schemes, cipher_suites (and local_ip when available) are added as query parameters, identical to the stock http getter. |
ttl |
duration | from response | How long a fetched certificate is cached. When omitted, the s-maxage of the response's Cache-Control header is used, falling back to 60s. |
negative_ttl |
duration | 60s |
How long a 204 ("no certificate for this name") response is remembered before re-querying. |
cache_dir |
path | (disabled) | If set, fetched certificate bundles are persisted here (0600 files in a 0700 directory) and loaded on startup, so the cache survives restarts. Contains private keys. |
- HTTP 200: response body is parsed as a PEM bundle (certificate chain + private key) and cached.
- HTTP 204: cached as a negative entry for
negative_ttl; the handshake falls through to Caddy's on-demand /askflow. - Any other status, or a parse error: returned as an error and not cached, so a transient upstream failure is retried on the next handshake.
- Concurrent cache misses for the same name are coalesced into a single upstream request.
- Each upstream fetch is bounded by a 10s timeout, and it does not inherit the per-handshake context, so one client disconnecting cannot cancel a fetch that other handshakes are waiting on.
- Entries loaded from
cache_diron startup are re-aged: they get a fresh TTL on load (the originalCache-Controlis not persisted), so a warm-restart entry re-validates within one TTL.
go test ./...
go test -race ./... # what CI runs
gofmt -l . # must print nothing
go vet ./...Requires Go 1.25 or newer.
- Point
cache_dirat a directory only the Caddy user can read; it stores private keys. The0700mode is applied only when the module creates the directory. - Keep the previous Caddy binary around for instant rollback after swapping in a build that includes this module.