Skip to content

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

Open
vsc55 wants to merge 3 commits into
FreePBX:release/17.0from
vsc55:release/17.0
Open

feat: Private ACME server support, installed-CA viewer, and CA install into the system trust store (freepbx 17)#15
vsc55 wants to merge 3 commits into
FreePBX:release/17.0from
vsc55:release/17.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.

vsc55 added 3 commits June 21, 2026 10:12
…I option to install a CA certificate into the system trust store (via privileged sysadmin hook).
- 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.
…neration and GraphQL API

System trust-store CA management
- Store trusted CAs as desired state in the module KVStore (Certman now
  extends \FreePBX\DB_Helper) instead of staging files; the privileged
  reconcile installs/removes anchors as root and refreshes the trust store.
- Two-step workflow in the "Installed CA's" page: load a certificate, then
  Install / Uninstall / Remove it from the system per-CA. Drift between the
  managed state and the OS trust store is flagged with a "Reconcile now" action.
- Simplify hooks/install-ca to a one-line privileged trigger that runs
  "fwconsole certificates --reconcile-system-cas"; add that console command.
- Self-heal on module install (reconcile as root) so CAs survive trust-store
  wipes (e.g. OS updates).

Installed CAs viewer
- Convert the managed-CAs and trusted-CAs tables to AJAX bootstrap-table grids
  (getManagedCAsGrid / getSystemCAsGrid) with search, sorting and refresh.
- Move the "Load a CA Certificate" form into a modal; reorder the page with
  Trust Stores on top; fix column proportions and action-button layout.

Backup / Restore
- Include the module KVStore in backups via dumpKVStore()/importKVStore() and
  reconcile the system CAs on restore.

Let's Encrypt generation
- Run new/edit LE generation in the background from a progress modal: launch via
  AJAX (generateLEStart), poll the captured log (generateLEStatus), show a
  coloured progress bar (success/error) and redirect to the main page on success;
  restore the toolbar Submit button on failure/close.
- Add a GraphQL mutation generateLetsEncrypt supporting the public service and a
  private/self-hosted ACME server (directory URL).
- Only the host name is required; the public service additionally requires
  country and email, a private ACME server (acmeUrl) needs only the host.
  Applied consistently across the web form, JS, CLI and GraphQL.

Cleanup
- Remove the obsolete DST Root CA X3 workaround (hooks/fix-le-root-ca, its
  install.php call, and the "Remove DST Root CA X3" option in the LE form).
- Drop the now-redundant #[\AllowDynamicProperties] (inherited from DB_Helper).
@vsc55

vsc55 commented Jun 27, 2026

Copy link
Copy Markdown
Author

Update — follow-up changes

This revision reworks the "install a CA into the system trust store" feature and adds several related capabilities. The headline change is moving the privileged CA install from a staging-file + incron hook design to a KVStore desired-state + reconcile model.

Why move CA management to the KVStore

The original design staged the PEM as a file and had the incron hook read it, copy it into the trust anchors and refresh. In practice that approach proved fragile:

  • Data coupled to the hook invocation. The PEM travelled through incron/the staging dir, so any problem in that path (permissions, the module dir not being reliably writable by the web user, the trigger format) silently broke the install.
  • The hook wasn't reliably executed. The Sysadmin incron runner only runs a hook that carries the right ownership/permissions, and chownFreepbx() never registered the hooks/ directory — so a newly added hook (install-ca) wasn't set up for the runner to execute.
  • No persistence and no backup. A trust-store wipe (e.g. an OS update re-running update-ca-trust) silently dropped the CA, and the staged file wasn't part of the module backup.

The KVStore model fixes all of this:

  • The trusted CAs are stored as desired state in the module KVStore (the class now extends \FreePBX\DB_Helper) — a single DB-backed source of truth that survives reboots/wipes and is included in the module backup.
  • The privileged step becomes an idempotent reconcile that reads the KVStore and installs/removes the certman-* anchors to match it. No data travels through incron: the hook (hooks/install-ca) is now a one-line trigger that runs fwconsole certificates --reconcile-system-cas as root.
  • chownFreepbx() now registers hooks/ (as execdir) so fwconsole chown gives the hook the ownership the runner requires.

✅ This resolves the previous Known limitation: the web → root → update-ca-trust/update-ca-certificates path now works on a properly deployed module (run fwconsole chown and ensure incrond is active).

CA management UI is now two-step

  • Load a certificate (validated, stored in the KVStore — no system change yet), then Install / Uninstall it from the system trust store per-CA, or Remove it entirely.
  • The Installed CAs page flags drift (managed but missing, or marked-for-uninstall but still present) with a Reconcile now action.
  • The trusted-CAs and managed-CAs tables are now AJAX bootstrap-table grids (search/sort/refresh); the "Load a CA Certificate" form moved into a modal; Trust Stores moved to the top.
  • New console command: fwconsole certificates --reconcile-system-cas. Reconcile also runs on module install (self-heal).

Backup / Restore

  • The module KVStore is now captured via dumpKVStore() / importKVStore() and the system CAs are reconciled on restore.

Let's Encrypt generation runs in the background

  • New/edit LE issuance now runs from a progress modal: launched via AJAX, with live log polling, a coloured progress bar (success/error) and redirect to the main page on success. The toolbar Submit button is restored on failure/close.

GraphQL API

  • New mutation generateLetsEncrypt supporting both the public service and a private ACME server (directory URL).

Field requirements (public vs private)

  • Only the host name is required in general. The public Let's Encrypt service additionally requires country and email; a private ACME server (acmeUrl set) needs only the host. Enforced consistently across the web form, JS, CLI and GraphQL.

Cleanup

  • Removed the obsolete DST Root CA X3 workaround: hooks/fix-le-root-ca, its install.php call, and the "Remove DST Root CA X3" option in the LE form (the CA expired in 2021 and the hook only applied to legacy SNG7/CentOS).
  • Dropped the now-redundant #[\AllowDynamicProperties] (inherited from DB_Helper).

New / notable files

  • hooks/install-ca — now a privileged reconcile trigger (was a staging-file installer).
  • views/systemcas.php — two-step CA management + drift + AJAX grids.
  • Backup.php, Restore.php — KVStore backup/restore.
  • Api/Gql/Certman.phpgenerateLetsEncrypt mutation.
  • Certman.class.php, Console/Certman.class.php, views/le.php, assets/js/certman.js.

@vsc55

vsc55 commented Jul 17, 2026

Copy link
Copy Markdown
Author

Hi @kapilgupta01,
Are contributions still accepted, or do I have to do it another way?
Best Regards.

@kapilgupta01

Copy link
Copy Markdown
Contributor

Hey @vsc55 I am no longer with Sangoma.

Looping @chrsmj to take care of this further. Thanks

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.

2 participants