Security model & deployment best practices

This document describes the security model of customsso (the FreePBX agent module) and customsso-manager (the standalone manager server), and the deployment practices that preserve it. Read this before pairing your first production PBX.

v1.0.0+ security hardening — what's new

The v1.0.0 release shipped the UCP admin-impersonation feature and closed a post-audit sweep for parity with the existing admin-SSO path:

Deferred to v1.0.1 (H-series audit finding): signed logout_ucp teardown endpoint + JS beforeunload sendBeacon so asterisk.ucp_sessions rows don't linger after Switch-user / tab close.

v0.6.4+ security hardening — what's new

The v0.6.4 release closed a full internal audit (3 CRITICAL + 10 HIGH findings). The changes most operator-visible:

Deferred (tracked as open items — planned for v0.7): - PBX-side HMAC secret split into a helper-daemon signing model so the secret is no longer readable by the asterisk group. - Full CSRF-token gate on /pbx/{uuid}/sso (current Referer-check is defense-in-depth only). - Two-phase atomic master-key rotation. - PBX-side session-fixation defense (needs investigation of FreePBX 17's session bootstrap interactions).

Layered defenses, ranked

Layer What Why it matters
1. Per-PBX unique keys Every paired PBX gets its own HMAC secret + ed25519 SSH keypair. None are shared across PBXes. Compromise of one pairing never compromises others. A manager DB row leak forces a per-PBX revoke loop, not a fleet-wide reset.
2. Encrypted-at-rest secrets on manager All *_ct columns (hmac_secret_ct, ssh_privkey_ct) are AES-256-GCM ciphertext under a master key. A read-only DB leak alone (SQL injection, backup tape) yields no usable credentials. The master key lives on disk in a separate file owned by apache:apache mode 0600.
3. Layered access controls on each PBX (a) OS firewall (firewalld/nftables/iptables — exported from the agent for the admin to apply), (b) SSH host-key pinning (TOFU'd at pair time, enforced on every sidecar connect — a re-imaged PBX offering a different host key is refused), (c) encrypted-at-rest SSH private key on the manager (ssh_privkey_ct AES-256-GCM), (d) SSO landing endpoint (/customsso-sso) accepts only HMAC-verified signed tokens with a 30s TTL and one-shot nonce. If any one layer is misconfigured, three more catch it. Note (v0.6.4+): the previous from="<manager_ip>" restriction on authorized_keys was dropped so DR-restore-to-a-different-IP works — host-key pinning + encrypted private key + HMAC-signed SSO token replace that network-layer defense. See BACKUP.md for the DR context. Defense in depth without brittle IP pinning.
4. One-time SSO tokens 30-second TTL, HMAC-SHA256 signature with constant-time compare, nonce stored in customsso_nonces (UNIQUE constraint = one-time-use). Replay-protected even on the manager → user → PBX redirect path.
5. SSH command allowlist for bulk operations The manager's Bulk/fwconsole dispatcher only runs commands in the built-in CMD_MAP or the admin-defined custom-commands catalog. Placeholder tokens (@MODNAME@, @LINES@) are validated before substitution. Bounds the blast radius of a compromised operator-role login. Note: an admin-role user can define new custom commands with arbitrary shell — role docs treat admin as fleet-wide root by design.
6. Manager-initiated pairing Pairing happens over SSH from the manager, not from a form on the PBX side. The operator's browser action never leaves the manager. No drive-by pairing — there is no PBX-side "accept this manager" wizard to phish.
7. Append-only audit log on both sides Every SSO redemption, every SSH job dispatch, every pairing event, every login. Manager-side has user_id attribution; PBX-side has source_ip attribution. Both also emit syslog lines suitable for fail2ban. Forensic trail post-incident.
8. fail2ban integration Bundled jail config on the manager (/etc/fail2ban/jail.d/customsso-manager.conf) bans IPs after 5 failed logins in 10 minutes. Agent's syslog format is parseable for a similar jail. Rate-limits credential stuffing.
9. Role-based access on the manager superuser / admin / operator / readonly. Only superusers can manage users and rotate the master key. Least-privilege for shared admin teams.
10. CSRF tokens on every state-changing POST Every write route validates Session::requireCsrf(); the token is per-session and embedded in each form. Blocks cross-site request forgery against authenticated admin sessions.

Hard requirements

These are not optional — pairing won't work or won't be secure without them:

  1. Enter the PBX root SSH password directly into the manager UI over HTTPS. Don't route it through Slack / email / anywhere with logs. The manager wipes it on the PBX immediately after pair completes; the window of vulnerability is measured in seconds.
  2. Set a public manager URL (Settings → Advanced → Public manager URL) before pairing if your manager is behind NAT or has a public-vs-internal hostname split. The pair handshake records this URL at the PBX; changing it later requires a push (also on the same Settings tab).
  3. Configure your OS firewall on each PBX to allow only the manager IP to reach ports 22, 80, and 443. The manager writes matching restrictions into the PBX's authorized_keys but a host firewall is your defense-in-depth.
  4. Back up /etc/customsso-manager/master.key offline. Without it the encrypted DB columns are unreadable. Store on a USB key, encrypted password manager, or a fireproof safe — somewhere physically separate from the manager host.
Practice Why
Run the manager on a dedicated host (small VM is fine) Reduces blast radius. Compromise of the manager OS root compromises every paired PBX's SSH access.
Use a VPN-only manager (e.g., WireGuard) reachable only by admin laptops Removes the manager from the public attack surface entirely. fail2ban becomes a defense-in-depth, not the front line.
Rotate the master key every 90 days via Settings → Advanced → Rotate master key Local-only re-encryption (PBXes aren't contacted); zero downtime; old key kept 1 hour as crash recovery.
Re-pair each PBX every 12 months Generates fresh SSH keypair + HMAC secret; invalidates any silent-leak window.
Use non-default SSH port on the manager (not 22) Reduces script-kiddie noise. SSH key auth + fail2ban handle real attackers regardless.
Enable disk encryption (LUKS / Tang+Clevis / TPM) on the manager Defends the encrypted-at-rest secrets against physical disk theft. Install.sh has an advisory check but doesn't force it (see "LUKS tradeoffs" below).
Run separate admin accounts per human operator Per-user audit attribution works only if each operator has their own login.
Restrict superuser to 1–2 people User creation + master-key rotation are the rare, audit-worthy operations.

LUKS tradeoffs

LUKS gives the strongest defense against disk theft but requires interactive boot password entry. Three deployment options:

  1. Headed/console-accessible host: full LUKS with prompt. Pick this when you have IPMI/iLO/console access.
  2. Headless with Tang/Clevis: LUKS volume auto-unlocks when the host can reach a "Tang" key-server on the network. Stolen + booted off-network → no unlock. Best of both worlds; requires running a Tang server somewhere on the LAN.
  3. No disk encryption + strict access control: app-level AES-256-GCM still encrypts secret columns. Master key lives on the same disk so this gives little protection against physical theft — defense becomes "the disk doesn't leave the rack."

Key rotation procedures

Master key (manager-local, all PBXes)

  1. Settings → Advanced → Rotate master key (superuser only)
  2. Manager re-encrypts every *_ct column under the new key in a single DB transaction. A mid-rotation crash rolls back cleanly.
  3. Old key saved to master.key.old.<timestamp> for 1 hour crash recovery.
  4. No PBX contact required.

Atomicity caveat (H10 deferred): the rotation is single-transaction best-effort — atomic two-phase rotation (concurrent old-key retention with a grace window for in-flight decrypts on other web workers) is not yet implemented. In practice this means: run rotation during a low-traffic window; a request that's mid-decrypt when rotation commits gets a decrypt failure and needs a page reload. Not a data-safety issue — the crash-recovery master.key.old.<ts> lets you roll back if anything looks wrong.

Per-PBX HMAC + SSH (full re-key)

Two paths — pick by whether you have SSH still working:

Path A — SSH still works (rotation as maintenance) 1. Manager → PBX detail → Rotate keys 2. Manager SSHes in with the current key, generates a fresh keypair + HMAC secret, installs them, updates the PBX-side manifest. Zero-touch operator experience.

Path B — SSH broken, needs root password (Re-pair) 1. Manager → PBX Status page → Re-pair button (visible only when status is offline) — OR — Settings → Reclaim (visible only when unpaired PBXes exist from a failed auto-reclaim during restore) 2. One-field form: PBX root password. Everything else (host, label, hardware node, notes) pre-populated from the existing DB row. 3. Sidecar SSHes in with the password, generates fresh keys + HMAC, installs the new customsso module version (if newer), rewrites manifest — preserves the pbxes.uuid so notes and node assignment survive.

Path C — Fleet-wide reset 1. Backup + restore on a new manager instance (see BACKUP.md). Auto-reclaim during restore handles every PBX in one pass.

GPG signing key (publisher identity)

The GPG key (046E8CA0EE6A755B) is used only to sign release tarballs. Rotation:

  1. Generate new key on the build machine; export new pubkey
  2. Update every published module's signatures/ bundle with the new pubkey
  3. Re-sign all current module versions, bump versions, publish
  4. Each PBX needs to import + trust the new pubkey before it'll trust new releases

Don't rotate the GPG key unless it's actually compromised — it's a heavy lift and breaks every existing trust chain.

Audit log discipline

What to do if you suspect compromise

If you suspect the manager is compromised:

  1. systemctl stop httpd customsso-sidecar on the manager to halt all outbound activity
  2. On each PBX: log in as root and run sudo /usr/local/bin/customsso-unpair — this reads the manifest and removes the manager's SSH key from /root/.ssh/authorized_keys plus every file the pair placed
  3. Manually verify /root/.ssh/authorized_keys has no customsso-manager marker lines left
  4. Examine the manager's audit table for the compromise window — look for bulk.dispatch, shell.enqueue, master_key.rotate you didn't authorize
  5. Rebuild the manager from scratch on a clean host; do not restore from backup (master.key may be in the backup)
  6. Re-pair fresh

If you suspect a single PBX is compromised:

  1. On that PBX: sudo /usr/local/bin/customsso-unpair
  2. On the manager: PBX detail → Unpair (double-revoke is fine)
  3. Investigate via FreePBX's own audit and the agent's customsso_audit table
  4. Re-pair from clean state

If the GPG signing key is compromised:

  1. Stop publishing immediately
  2. Revoke the key in your public keyserver (if you've published one)
  3. Generate a new key, re-sign every current module, bump versions, publish
  4. Every existing PBX needs to import the new pubkey + delete the old trust; this is a fleet-wide manual operation

Out of scope — explicitly NOT defended against

See THREAT_MODEL.md for the explicit list. Highlights: