SSL

Automatic HTTPS for every site with ACME, wildcard certificates, renewal, custom certificates and CSRs.

In preview ssl version 0.1.0 Part of SSL certificates

Actions

17 actions, callable from the panel, the command palette and the API as POST /api/v1/a/<id>. Internal actions used between modules are not listed.

ActionWhat it doesRiskPreview
ssl.ca.list List certificate authorities (read) low No dry run
ssl.ca.add Add a certificate authority high No dry run
ssl.ca.update Change a certificate authority high No dry run
ssl.ca.remove Remove a certificate authority high No dry run
ssl.ca.test Test a certificate authority (read) low No dry run
ssl.cert.issue Issue an ACME certificate low
ssl.cert.renew Renew now (or retry) low
ssl.cert.upload Upload a custom certificate medium No dry run
ssl.cert.attach Attach a certificate to a host medium No dry run
ssl.cert.delete Delete a certificate medium
ssl.cert.revoke Revoke a certificate high
ssl.cert.list List certificates (read) low No dry run
ssl.cert.get Get a certificate (read) low No dry run
ssl.csr.create Generate a CSR low No dry run
ssl.selfsigned.create Create a self-signed certificate low No dry run
ssl.check Check the served certificate (read) low No dry run
ssl.https.set Force HTTPS for a site low No dry run

Permissions and limits

Permissions

  • ssl.cert.view View certificates
  • ssl.cert.issue Issue and renew ACME and self-signed certificates
  • ssl.cert.upload Upload custom certificates
  • ssl.cert.attach Attach certificates to hosts
  • ssl.cert.delete Delete certificates
  • ssl.cert.revoke Revoke certificates
  • ssl.csr.manage Generate CSRs
  • ssl.https.manage Force HTTPS
  • ssl.ca.manage Manage certificate authorities

Plan limits

  • ssl.custom_certs Uploaded certificates
  • ssl.acme_certs ACME certificates
  • ssl.wildcards Wildcard certificates
  • ssl.san_per_cert Names per certificate

Engineering notes

Generated from modules/ssl/docs.md at build d90e9e2. These are the notes the engineers keep next to the code: precise, technical, and honest about what is not done yet.

ACME order manager in the control-plane plugin on lego v4 (MIT) used as a library; spec docs/specs/ssl.md. Desired/observed state lives in schema m_ssl (cas, certs, cert_history): only public material.

Keys never leave the node (ADR 0012 §6)

The agent (internal/agent/ssl.go) generates the key (ECDSA P-256 default, P-384, RSA 2048/3072/4096) and returns a CSR; the plugin runs lego ObtainForCSR with it. The key waits in /var/lib/respirecloud/ssl/pending/<cert id>.key (0600) until the signed chain arrives, then ssl.cert.install checks the public keys match, promotes it to /var/lib/respirecloud/ssl/certs/<id>/{fullchain,privkey}.pem (previous kept as .prev) and deploys from disk to the targets:

  • web: calls the web driver's own webCertInstall in-process (same agent binary), so web's validate/swap/health/rollback path is used and the key never travels over the bus (web's web.sites.cert.install action would need it in params). The agent then systemctl reload nginx: the web driver does not reload when the rendered config is unchanged, so a renewal would otherwise keep serving the old certificate from memory (found in the lab).
  • mail: /etc/rc-mail/tls/{fullchain,privkey}.pem (the path mail's installer self-signs at), then reload of postfix and dovecot.
  • panel: /etc/respirecloud/tls/panel/{fullchain,privkey}.pem (no reload; core wiring is a follow-up). Errors, events, audit and API results never contain key material or raw CA text (sslx.Classify maps failures to plain words). Uploads (custom certs) are the one place a user-supplied key reaches the plugin as a param (redacted in the audit); it is forwarded to the agent and not stored in the database or returned.

Orders

ssl.cert.issue (dry-run plan) -> row pending -> runIssue: lease lock (10 min) -> CAs in order (pinned CA = no fallback; rate-limited CAs last; fall back on outage/rate-limit/CAA, not on validation failures) -> per-CA mutex (orders to one CA are serialised) -> CSR on node -> HTTP-01 (agent writes the token under /var/lib/rc-acme/.well-known/acme-challenge/, web serves it) or DNS-01 (dns.acme_challenge.set|clear as the system actor, 2 s fixed wait, our own authoritative server answers) -> ssl.cert.install on the node. Wildcards always use DNS-01 and need the zone hosted in dns (else precondition_failed naming it). Failure: row keeps the previous certificate active, renewal_state = waiting_dns | rate_limited | failed | deploy_failed, plain-words last_error, backoff 5 m / 15 m / 1 h / 3 h / 6 h (rate limit >= 1 h), event ssl.cert.failed. ssl.cert.renew (force) = "Retry now". If the CA issued but a target refused it, the chain is recorded and only the deploy is retried. ARI: after issuing, GetRenewalInfo -> a random instant inside the CA's suggested window becomes renew_at (ari=true, and the next order sends replaces); without ARI renew_at = notAfter - min(30 d, lifetime/3) (a 6-day cert renews on day 4).

Auto-SSL (subscriptions, durable, idempotent)

  • web.site.created|updated (ssl.on_site_created|updated, one action per event: shared action names share a consumer): skips web's own certificate: installed update; keeps a cert already attached that covers every name; else attaches an active cert of the account that covers all names (e.g. a wildcard); else orders one cert for all names of the site and, when it is active, deletes older ACME certs of that site (alias added -> one combined cert, the site always has a certificate).
  • domains.created for alias|parked: web adds aliases without emitting web.site.updated, so ssl waits (fails, redelivered) until the site lists the alias and then runs the same check. For apex domains and setting auto_wildcard it orders apex + *.apex.
  • web.site.deleted strips the target (and deletes a non-wildcard ACME cert with no target left); domains.deleted stops renewal and deletes after 7 days (certs that only cover that domain); accounts.deleted removes the account's certs and node files.
  • Scheduler ssl.process_due (every 1 m, system): marks expired, emits ssl.cert.expiring at 14/7/3/1 days for certs whose renewal is failing, runs due renewals and due retries (25 per run), deletes after grace.

Actions

CA: ssl.ca.list|add|update|remove|test (admin, high risk; EAB HMAC and the ACME account key live in the host vault as ca_eab:<id> / ca_key:<id>; the account registers on first use). Certs: ssl.cert.issue|renew|delete|revoke (dry-run plans), ssl.cert.upload (PEM; key match, expiry, weak key, coverage, chain order reported; without a key it matches a pending CSR by public key), ssl.csr.create, ssl.selfsigned.create (flagged untrusted), ssl.cert.attach, ssl.cert.list|get, ssl.check (agent connects to 127.0.0.1:443 with SNI, reports the served cert and whether it verifies), ssl.https.set (calls web.sites.update force_https). Limits: ssl.wildcards (reserved per wildcard cert), ssl.custom_certs (per upload), ssl.san_per_cert (setting san_per_cert, default 100), ssl.acme_certs unlimited. Settings (core.settings scope module:ssl): node_id, email, key_type, auto_ssl (default true), auto_wildcard, san_per_cert. Events: ssl.cert.issued|renewed|failed|expiring|revoked, ssl.ca.ratelimited.

Contract for other modules

  • Any module may call ssl.cert.issue {hosts[], target: mail|panel|none, account_id?} (admin/system for mail/panel) and ssl.cert.attach {cert_id, target: mail|panel | site_id}; renewals redeploy to every recorded target automatically.
  • mail: installs/reloads at /etc/rc-mail/tls as above; mail should call ssl.cert.issue {hosts:[<mail hostname>], target:"mail"} after mail.install (not wired by this card). panel/core: target:"panel" writes /etc/respirecloud/tls/panel/; core must read it and reload (not wired).
  • web (requests): (1) has_certificate was false after install in the lab (the _cert layer row is not persisted), ssl does not rely on it; (2) emit web.site.updated when an alias is added/removed (ssl works around it via domains.created); (3) reload nginx when only a certificate file changed, or accept a cert_ref instead of key bytes in web.sites.cert.install.

Verified (lab w5-01, Ubuntu 24.04, Pebble as the CA, lego 4.35.2)

See docs/tasks/w5/w5-01-ssl.handover.md for the commands and outputs.

Not done / next

PKCS#12 upload; CAA pre-check (the CA's refusal is classified instead); external DNS providers (lego providers) for DNS-01; TLS policy (ssl.policy.set, min version/ciphers/OCSP stapling); HSTS toggle (web setting http.hsts_max_age); shared-cert/LB key distribution (vault); CT monitor; on-demand TLS ssl.ondemand.ask; per-node deploy state for clusters (a cert is deployed on the one node that serves its site); shared certs losing a name when a domain is removed; rate-limit dashboard (state is in cas.ratelimited_until); UI; ARI re-polling between issue and renew time; renewal notifications to users (events only).