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:
- 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.
- 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). - 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_keysbut a host firewall is your defense-in-depth. - Back up
/etc/customsso-manager/master.keyoffline. 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.
Recommended deployment practices
| 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:
- Headed/console-accessible host: full LUKS with prompt. Pick this when you have IPMI/iLO/console access.
- 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.
- 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)
Settings → Advanced → Rotate master key(superuser only)- Manager re-encrypts every
*_ctcolumn under the new key in a single transaction - Old key saved to
master.key.old.<timestamp>for 1 hour crash recovery - No downtime, no PBX contact
Per-PBX HMAC + SSH (full re-key)
- On the manager: PBX detail → Unpair
- Re-pair via the normal Add PBX form (you'll need the PBX's root password again, one time)
- 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:
- Generate new key on the build machine; export new pubkey
- Update every published module's
signatures/bundle with the new pubkey - Re-sign all current module versions, bump versions, publish
- 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
- Read the audit log weekly — at minimum check for unexpected
login.fail,sso.issueoutside business hours,bulk.dispatchyou didn't initiate - Export to CSV monthly for offline retention (Audit → CSV button)
- Don't disable retention in production — the vacuum loop trims audit at 365 days by default; shorter is fine, longer needs more disk
What to do if you suspect compromise
If you suspect the manager is compromised:
systemctl stop httpd customsso-sidecaron the manager to halt all outbound activity- 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_keysplus every file the pair placed - Manually verify
/root/.ssh/authorized_keyshas nocustomsso-managermarker lines left - Examine the manager's
audittable for the compromise window — look forbulk.dispatch,shell.enqueue,master_key.rotateyou didn't authorize - Rebuild the manager from scratch on a clean host; do not restore from backup (master.key may be in the backup)
- Re-pair fresh
If you suspect a single PBX is compromised:
- On that PBX:
sudo /usr/local/bin/customsso-unpair - On the manager: PBX detail → Unpair (double-revoke is fine)
- Investigate via FreePBX's own audit and the agent's
customsso_audittable - Re-pair from clean state
If the GPG signing key is compromised:
- Stop publishing immediately
- Revoke the key in your public keyserver (if you've published one)
- Generate a new key, re-sign every current module, bump versions, publish
- 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:
- Compromised admin workstation: a keylogger or session hijack on the admin's laptop has all the access the admin has. Use a clean workstation, FIDO2 / hardware-token-protected SSH keys.
- Compromised manager host root: the master key sits on the manager. Root on the manager = read all secrets. Defense is host hardening, dedicated VM, audit, restricted access.
- Compromised FreePBX core on a paired PBX: if FreePBX itself is compromised, the agent module can't protect against actions taken in its name. We sign + verify our own module but not the rest of FreePBX.
- Side-channel attacks against the GPG key: assumed not applicable since the key is on an air-gapped or admin-only build machine.