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.
| Action | What it does | Risk | Preview |
|---|---|---|---|
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.viewView certificatesssl.cert.issueIssue and renew ACME and self-signed certificatesssl.cert.uploadUpload custom certificatesssl.cert.attachAttach certificates to hostsssl.cert.deleteDelete certificatesssl.cert.revokeRevoke certificatesssl.csr.manageGenerate CSRsssl.https.manageForce HTTPSssl.ca.manageManage certificate authorities
Plan limits
ssl.custom_certsUploaded certificatesssl.acme_certsACME certificatesssl.wildcardsWildcard certificatesssl.san_per_certNames 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 ownwebCertInstallin-process (same agent binary), so web's validate/swap/health/rollback path is used and the key never travels over the bus (web'sweb.sites.cert.installaction would need it in params). The agent thensystemctl 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), thenreloadof 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.Classifymaps 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 owncertificate: installedupdate; 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.createdforalias|parked: web adds aliases without emittingweb.site.updated, so ssl waits (fails, redelivered) until the site lists the alias and then runs the same check. For apex domains and settingauto_wildcardit orders apex +*.apex.web.site.deletedstrips the target (and deletes a non-wildcard ACME cert with no target left);domains.deletedstops renewal and deletes after 7 days (certs that only cover that domain);accounts.deletedremoves the account's certs and node files.- Scheduler
ssl.process_due(every 1 m, system): marks expired, emitsssl.cert.expiringat 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) andssl.cert.attach {cert_id, target: mail|panel | site_id}; renewals redeploy to every recorded target automatically. - mail: installs/reloads at
/etc/rc-mail/tlsas above; mail should callssl.cert.issue {hosts:[<mail hostname>], target:"mail"}aftermail.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_certificatewas false after install in the lab (the_certlayer row is not persisted), ssl does not rely on it; (2) emitweb.site.updatedwhen an alias is added/removed (ssl works around it viadomains.created); (3) reload nginx when only a certificate file changed, or accept acert_refinstead of key bytes inweb.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).