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.

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. Three-layer IP enforcement on each PBX (a) OS firewall (firewalld/nftables/iptables — exported from the agent for the admin to apply), (b) SSH from="<manager_ip>" directive in /root/.ssh/authorized_keys restricting the manager's key to a single source IP, (c) SSO landing endpoint (/customsso-sso) accepts only signed tokens whose HMAC validates. If any one layer is misconfigured, two more catch it.
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 transaction
  3. Old key saved to master.key.old.<timestamp> for 1 hour crash recovery
  4. No downtime, no PBX contact

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

  1. On the manager: PBX detail → Unpair
  2. Re-pair via the normal Add PBX form (you'll need the PBX's root password again, one time)
  3. New HMAC secret + SSH keypair are auto-generated + installed on the PBX

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: