Skip to content

feat: Private ACME server support, installed-CA viewer, and CA install into the system trust store (freepbx 16) - #14

Open
vsc55 wants to merge 2 commits into
FreePBX:release/16.0from
vsc55:release/16.0
Open

feat: Private ACME server support, installed-CA viewer, and CA install into the system trust store (freepbx 16)#14
vsc55 wants to merge 2 commits into
FreePBX:release/16.0from
vsc55:release/16.0

Conversation

@vsc55

@vsc55 vsc55 commented Jun 21, 2026

Copy link
Copy Markdown

Summary

Adds the ability to issue Let's Encrypt certificates against a private / self-hosted ACME server (not only the public service) over the existing http-01 challenge, plus tooling to manage the CA certificates the server trusts.

Motivation: many deployments run an internal ACME-compatible CA (step-ca, Boulder, Pebble, …) and need certman to talk to it instead of acme-v02.api.letsencrypt.org.

What's included

1. Private / self-hosted ACME server support

  • New per-certificate fields in the Let's Encrypt form (and fwconsole options):
    • Custom ACME Server URL — the full ACME directory URL. Empty = public Let's Encrypt (unchanged behaviour).
    • ACME Server CA Bundle — optional PEM CA bundle for servers whose TLS endpoint is signed by a private CA.
    • Skip ACME Server TLS Verification — optional, for self-signed setups on trusted networks.
  • New AcmeHttpClient transport implementing Analogic\ACME\ClientInterface (does not patch the vendored lescript, so it survives composer updates). It supports an explicit directory URL (so non-standard paths like step-ca's /acme/<provisioner>/directory or Pebble's /dir work), a custom CA bundle (CURLOPT_CAINFO) and an optional insecure mode.
  • updateLE() points lescript at the configured directory and skips the public mirror1.freepbx.org reachability probe when a custom server is used (it's only meaningful for the public service). Settings are persisted per certificate and inherited automatically by the renewal paths (cron + web).
  • CLI: fwconsole certificates --generate --type le ... --acme-url=<dir> [--acme-ca=<pem>] [--acme-insecure].

2. Installed-CA viewer

  • New Installed CAs page (linked from the certificate list) listing the CA certificates trusted by this server: trust-store paths with certificate counts, and a searchable table of each CA (CN, issuer, root/intermediate, expiry, SHA-1 fingerprint, source file).
  • Detects the distribution family (Debian vs RHEL) and shows only the relevant trust stores; falls back to showing all when it can't be determined. The active PHP/cURL bundle is always shown.
  • Purpose: confirm a private ACME server's CA is already trusted and locate a usable CA bundle path.

3. Install a CA into the system trust store (web + CLI)

  • A collapsible Install a CA Certificate card on the Installed CAs page (paste PEM or upload a file).
  • Because the web process is unprivileged, the install runs as root via a new Sysadmin incron hook (hooks/install-ca): the web side stages the PEM and the hook copies it into the distribution trust anchors and runs update-ca-trust / update-ca-certificates. Installation is confirmed by re-scanning the trust store by fingerprint.
  • CLI: fwconsole certificates --install-ca=/path/to/ca.pem (runs the hook directly when root).
  • Requires the Sysadmin module; the UI states this clearly.

Security notes

  • Installing a CA into the system trust store is a powerful, admin-only action (a malicious CA enables TLS interception); the form warns about this.
  • TLS verification of the ACME server stays on by default; "insecure" is opt-in per certificate.

Files of note

  • Acme/AcmeHttpClient.php (new) — custom ACME transport.
  • hooks/install-ca (new) — privileged CA install hook.
  • views/systemcas.php (new) — Installed CAs page + install form.
  • Certman.class.php, Console/Certman.class.php, views/le.php, views/certgrid.php, assets/js/certman.js, module.xml.

Testing

  • Public Let's Encrypt issuance/renewal: unchanged (custom fields empty → original code path).
  • Private ACME (step-ca / Pebble) over http-01: issued and renewed against the configured directory.
  • Installed CAs page renders the trust store on Debian and RHEL family systems.
  • CA install via web (Sysadmin hook): pending — blocked on an unsigned dev build (see Known limitations). The fwconsole --install-ca path (root, no hook) works.

Known limitations / pending testing

⚠️ CA install into the system trust store is not yet fully verified.
The privileged install relies on the Sysadmin incron hook runner, which only
executes hooks from a signed module. On an unsigned/development build of
this module the hooks/install-ca hook did not run, so the end-to-end "Install
a CA" flow (web upload → root hook → update-ca-trust/update-ca-certificates)
still needs to be validated on a properly signed module.

Not affected: the private ACME server support and the Installed-CAs viewer work
independently of the hook. When running as root, fwconsole certificates --install-ca=<pem> also bypasses the hook (it invokes the install logic
directly) and can be used to verify the install path in the meantime.

…I option to install a CA certificate into the system trust store (via privileged sysadmin hook).
@vsc55 vsc55 changed the title I option to install a CA certificate into the system trust store (via privileged sysadmin hook). (freepbx 16) feat: Private ACME server support, installed-CA viewer, and CA install into the system trust store (freepbx 16) Jun 21, 2026
- Escape the certificate CN in result messages (it is rendered as HTML),
  preventing stored XSS from a crafted certificate subject.
- Reject private keys, multi-certificate bundles and oversized input;
  only a single CA certificate may be installed at a time.
- Trust the privileged hook's result: it now checks the exit code of
  update-ca-trust/update-ca-certificates, rolls back the anchor on
  failure, and the PHP side reports success only on a real "OK".
- Restrict the root hook to the staged file via an explicit argument,
  wipe stale staged files before each run, and ignore staged files
  older than 5 minutes (defence in depth).
- Include the certificate fingerprint in the stored filename to avoid
  collisions between different CAs sharing a Common Name.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant