Backends
What ships today, what's coming, and the design choices behind each.
CA backends
Namecheap (traditional reissue)
kind: namecheap with ssl_id: <numeric SSL id>. Uses Namecheap's namecheap.ssl.reissue and namecheap.ssl.getInfo flow. Activation is one-shot: rota only handles reissue within an existing SSL subscription. First-time activation requires a long list of admin-contact fields rota does not model in the config. Operators activate once by hand in the Namecheap dashboard; rota handles every renewal after that.
DNS-01 only. The Namecheap reissue API folds every SAN under one DCV record, so rota's multi-challenge trait sees a single-element vec.
ACME (RFC 8555)
kind: acme. Speaks Let's Encrypt, ZeroSSL (with External Account Binding), BuyPass, and any directory that follows the spec. Uses the instant-acme crate.
rota manages its own persistent ECDSA key per cert because operators rely on key continuity for cert pinning. The ACME submit path uses finalize_csr(csr_der) so the operator's key stays canonical across renewals.
The ACME backend walks the configured DCV solver's supported_kinds() to pick a challenge type per authorization. So dcv: { kind: webroot } automatically gets HTTP-01; dcv: { kind: cloudflare } automatically gets DNS-01.
DCV backends
Namecheap DNS
kind: namecheap. DNS-01 via namecheap.domains.dns.{getHosts,setHosts}.
Watch out: Namecheap's setHosts is a full replacement of every record on the domain, not a per-record edit. Publishing one TXT therefore requires reading every existing record first, merging the new one in, and writing the merged set back. rota does this transparently.
Cloudflare DNS
kind: cloudflare. DNS-01 via Cloudflare's v4 API with Bearer-token auth. Token scopes: Zone.DNS:Edit on every zone rota will manage. rota does not support the legacy Global API Key.
Cloudflare's per-record edit API means rota doesn't have to read every record on the zone first. The flow: resolve the apex zone for the record name, look for an existing TXT match (idempotency), POST the record if absent. Removal mirrors the lookup-and-delete shape.
Webroot (HTTP-01)
kind: webroot with directory: <document root>. rota writes the key authorization to <directory>/.well-known/acme-challenge/<token> (mode 644) and removes it after issuance. The operator's existing webserver (nginx, Caddy, Apache, anything that serves static files over HTTP on port 80) is responsible for actually exposing the directory.
Why webroot rather than a daemon-internal listener: most self-hosters already run a webserver on 80 and 443. Asking rota to bind 80 means coordinating port handoff (or running rota as root) for one purpose: serving a five-byte file the existing webserver could serve in its sleep.
Defensive against malformed challenge tokens: rota refuses path-shaped tokens (/, \, .., empty) so a misbehaving CA can't traverse out of the challenge directory.
Install backends
Synology DSM
kind: dsm with description: <DSM panel label>. Uses synowebapi to install the cert into DSM's certificate store. The cert id surfaces in the DSM Control Panel under the configured description.
Filesystem
kind: filesystem with directory: <path>. Lays the issued cert, chain, and private key down under predictable filenames so any service that reads disk-based PEM (nginx, HAProxy, Caddy, custom Rust + rustls) can pick them up.
Filenames mirror the certbot convention so existing reload scripts that grep for fullchain.pem and privkey.pem work unchanged. Writes are atomic per file: each artifact goes to a sibling .tmp, fsync, rename.
nginx
kind: nginx with directory: <path> and optional reload_command: [<argv>]. Filesystem write plus an nginx reload subprocess.
Default reload is ["nginx", "-s", "reload"]. Operators on systemd typically override with ["systemctl", "reload", "nginx"] and a sudoers rule that keeps the daemon unprivileged. The reload runs without a shell wrapper, so argv entries are not interpreted: no globbing, no env interpolation. A non-zero exit surfaces as an Install error so the renewer records the failure on the audit log.
HAProxy
kind: haproxy with directory:, socket_path:, and cert_storage_name:. Filesystem write plus HAProxy runtime API hot-swap.
The runtime API sequence:
set ssl cert <storage_name> <<EOL
<leaf + chain + key bundle>
EOL
commit ssl cert <storage_name>
No reload, no dropped TCP connections. HAProxy hands the new certificate to live SNI lookups on the next handshake. Requires HAProxy 2.x or later with the admin socket exposed:
global
stats socket /run/haproxy/admin.sock mode 660 level admin
Kubernetes Secret
kind: k8s_secret with namespace:, secret_name:, and optional kubeconfig_path:. Server-side applies a kubernetes.io/tls Secret. Drop-in for Ingress, Gateway, and any controller that consumes the standard TLS Secret shape.
Auth resolution:
kubeconfig_pathomitted: in-cluster ServiceAccount (run rotad as a Pod).kubeconfig_pathset: load the named kubeconfig (run rotad outside the cluster).
Required RBAC on secrets in the target namespace: get, create, patch. Server-side apply with FieldManager "rota" so concurrent managers (cert-manager, helm, etc.) get clean conflict signaling rather than silent overwrites.
Alert backends
kind: email. Lettre-backed SMTP. Submission ports (587 STARTTLS, 465 SMTPS) both supported via tls:. Auth is username + password from a file the daemon reads at runtime; the password never sits in the parsed config tree.
Webhook
kind: webhook. POSTs a generic JSON envelope to a URL:
{"cert_id": "...", "kind": "renewal_failed", "message": "...", "timestamp": "RFC3339"}
Vendor-neutral on the wire. Slack-incoming, Discord, Microsoft Teams, and similar opinionated formats are out of scope: point a small relay (n8n, Pipedream, your own service) at this URL and translate. Keeps rota's wire format flat instead of growing a per-vendor bestiary.
Optional Bearer token auth from a file. Per-request timeout defaults to 10 seconds.
Roadmap
More CAs: Sectigo direct, GoDaddy. More DNS-01 solvers: Route 53, DigitalOcean, Porkbun. More install targets: a native HTTP-01 listener (instead of webroot), more reload integrations as operators surface needs.