Threat model
Explicit statement of what customsso + customsso-manager defend against, what they don't, and what assumptions the security model rests on. Written for community / security review.
Assets being protected
Listed roughly by sensitivity, highest first.
- The GPG signing key (
046E8CA0EE6A755B) — controls what code every PBX trusts as "VoIP Stuff". A leak = ability to publish malicious updates to every paired PBX. - The manager's master AES key (
/etc/customsso-manager/master.key) — decrypts every per-PBX HMAC secret and SSH private key stored in the manager DB. A leak + DB access = fleet-wide control. - Per-PBX SSH private key — the only auth channel between manager and PBX. Command-exec-only (
no-pty,no-X11,…), host-key-pinned on the manager side. AES-256-GCM encrypted at rest on the manager under the master key. - Per-PBX HMAC secret — ability to mint valid SSO landing tokens for that PBX. Since v1.0.0, tokens carry a
variantclaim (adminorucp) that selects which session-mint path runs on the PBX, so this secret can be used to become eitheradminOR any UCP-loginable user. - Manager admin credentials — bcrypt-hashed; one human's compromised account = ability to use whatever role they have.
- The audit log — read-only forensic record; integrity matters more than confidentiality.
Threat actors considered
| Actor | Capabilities assumed | In scope? |
|---|---|---|
| Unauthenticated internet attacker | Can reach manager + PBX network ports | ✅ Yes — primary threat |
Authenticated low-privilege user on the manager (operator / readonly) |
Valid login, restricted role | ✅ Yes — least-privilege model |
| Attacker who obtains the SSH root password at the moment of pair (window: seconds, then wiped) | Same as legitimate operator during pair window | ✅ Yes — race-to-pair concern |
| Compromised single PBX | Root on one paired PBX | ✅ Per-PBX-isolation — that PBX's keys leak; others stay safe |
| Compromised manager OS (root) | Full root on the manager host | ❌ Out of scope — root has the master key |
| Compromised admin workstation | Keylogger / session hijack on the operator's laptop | ❌ Out of scope — has whatever the operator has |
Insider with legitimate superuser access |
Acting maliciously | ⚠️ Partial — every action audited, but actions are not prevented |
| Sangoma/FreePBX itself going hostile | Upstream framework backdoors the agent's host environment | ❌ Out of scope — we run inside FreePBX |
| Nation-state with cryptographic novel attacks | Breaks AES-256-GCM, ed25519, SHA-256, bcrypt | ❌ Out of scope |
Specific attack scenarios + our defenses
1. Public-internet attacker probes the manager UI
- What they can do: hit
/login - Defense: bcrypt password hashing (no plaintext, no SHA1), fail2ban after 5 failures, session cookie is HttpOnly+Secure+SameSite=Lax+server-side-validated (not just signed)
- What still works for them: dictionary attacks at the fail2ban rate (5/10min = 720/day)
- Mitigation: long random passwords + the recommendation to run the manager VPN-only
2. Public-internet attacker probes the PBX at network level
- What they can do: hit the PBX's public SSH port + Apache
- Defense layers:
- The manager's ed25519 pubkey is installed with
no-pty,no-X11,no-agent-forwarding,no-port-forwarding— command-execution only, no interactive shell hijack. (The pre-v0.6.4from="<manager_ip>"source-IP restriction was dropped to enable DR-restore to a different manager IP; see SECURITY.md #3 for the layered controls that replaced it.) - SSH host-key pinning: the manager captures each PBX's host key at pair time (TOFU) and refuses to connect if it ever changes — any MITM or PBX re-image trips a "host key changed" abort in Paramiko.
- The PBX has no inbound command endpoint the manager calls; every manager-initiated operation is an SSH exec.
- The SSO landing endpoint (
/customsso-sso) accepts only manager-signed tokens whose HMAC validates against the PBX's stored per-PBX secret, with a 30 s TTL and one-shot nonce. - What still works for them: whatever else the PBX exposes at the OS/FreePBX layer, which is outside this project's threat surface
3. Network attacker MITM's manager → PBX traffic
- What they can do: read/modify traffic if TLS is off
- Defense: SSH channel is authenticated + encrypted end-to-end at the SSH protocol layer. Host key pinning happens on the manager side at pair time; any MITM after that trips a "host key changed" abort in Paramiko.
- Mitigation: use TLS for the SSO landing endpoint too (Apache config generated at pair time terminates TLS). For internet-spanning fleets, ensure both endpoints have valid certs so browsers don't downgrade.
4. SSH root password intercepted during pair
- What they can do: use the password to log in ahead of the manager, e.g. install a persistence rootkit
- Defense: the pair job runs immediately after form submission and wipes the root password on completion (
passwd -d root). The window is measured in seconds. - Mitigation: paste the password directly into the manager UI over HTTPS; don't route it through Slack / email; use single-use rotated bootstrap passwords per pair. Watch the pair transcript at
/pair/progress/{job}— it prints every command the sidecar runs.
5. Compromised single PBX tries to attack the manager
- What they can do: send arbitrary requests to the manager
- Defense: the manager exposes only the SSO landing endpoint over HTTP, and paired PBXes never call it. All manager → PBX traffic is SSH; there is no inbound channel from PBX to manager for the agent to attack.
- What still works for them: nothing usable. The manager doesn't even listen on a port the agent uses.
6. SQL injection on the manager web app
- What they can do: in principle, dump tables
- Defense: every query in the manager goes through
Db::exec()/Db::all()/Db::one()which uses PDO prepared statements with parameterized queries. There is no string-concatenated SQL anywhere in the codebase. - Mitigation: keep grep-auditable —
grep -n "Db::pdo()->query" src/should match only static queries (no user input concatenation) - What still works for them: even a successful read of
pbxesreturns ciphertext for every secret column. Useful only with the master key.
7. XSS on the manager web app
- What they can do: inject script via stored fields (PBX label, command output)
- Defense: every value rendered in a view goes through
htmlspecialchars()(aliased as$h). Templates use<?= $h(...) ?>consistently. - Mitigation: a stored XSS would still require an admin to be logged in to fire — at which point session theft is the prize. Mitigated by
HttpOnlycookies. - What still works for them: nothing usable as long as the templates discipline holds.
8. CSRF against the manager
- What they can do: get an authed admin's browser to POST to
/bulkor/settings/users/<id>/toggle - Defense: every state-changing POST goes through
Session::requireCsrf()which validates a per-session token embedded in the form. SameSite=Lax cookies are the belt to the CSRF suspenders. - What still works for them: nothing usable — the token isn't recoverable from a cross-site position.
9. Compromised SSH key for one PBX
- What they can do: with the manager's per-PBX SSH private key (extracted from the manager's DB + master key), root shell on that PBX from anywhere on the network.
- Defense: the key on the manager side is AES-256-GCM ciphertext (
ssh_privkey_ct) under a master key stored in a separate root-owned file; a DB dump alone is useless. On the PBX side theauthorized_keysline isno-pty,no-X11,…which means command exec only, not interactive shell. Host-key pinning means a compromised key still can't be used to MITM the manager's next legitimate connection. - Historical note: prior to v0.6.4 the
from="<manager_ip>"restriction onauthorized_keysalso required the attacker to control the manager's source IP. That restriction was dropped to enable DR-restore-to-a-different-IP (see BACKUP.md). The layered controls above replace it. - What still works for them: if they get both the private key and the master key (root on the manager host), full root on every paired PBX. This is out-of-scope by design — root on the manager IS fleet root.
- Mitigation: protect the manager host root account (see SECURITY.md #recommended deployment practices), consider periodic master-key rotation via Settings → Master key rotation.
10. Malicious admin with superuser role
- What they can do: add user accounts, rotate keys, dispatch destructive bulk actions
- Defense: every action audited with user_id; can't disable themselves; can't disable the last superuser
- What still works for them: any action a superuser is supposed to be able to do, by definition
- Mitigation: limit superuser count, multi-person review for destructive bulk actions (not enforceable in code; org policy)
11. Malicious or compromised admin abuses UCP impersonation (v1.0.0+)
- What they can do: mint UCP-variant SSO tokens for any UCP-loginable user on any paired PBX, land in that user's UCP session, browse voicemail / call history / recordings / contacts.
- Defense:
Sso::launchUcprequiresSession::requireRole('admin')+ CSRF token. Every mint success writes asso.issueaudit row (variant=ucp, target_user, operator uid); every reject writessso.ucp_blockedwith reason, submitted target_user, remote IP, and User-Agent (enumeration is auditable). PBX-side re-checks effectiveAllow UCP Login?before establishing the session. UCP session is IP-pinned to the operator's browser via\FreePBX::Ucp()->storeToken(..., $srcIp). - What still works for them: what an admin is by definition supposed to be able to do — the impersonation is the feature. Attribution is retained by the audit log.
- Mitigation: limit
adminrole count; review the sso.issue / sso.ucp_blocked audit stream regularly; disable Allow UCP Login? for accounts an admin should never impersonate (executive extensions, security cameras, etc.); enable off-host audit-log forwarding to prevent audit tampering.
12. Attacker steals an operator's browser session mid-UCP-impersonation
- What they can do: with a walk-away browser or session hijack, follow the operator directly into the UCP iframe or navigate to
/ucp/on the target PBX and inherit the impersonated session until UCP's own TTL expires the token. - Defense: UCP session is IP-pinned — a hijacked session token used from a different browser IP is rejected by UCP.
- What still works for them: same-IP hijack (physical workstation walkup, same-NAT collaborator) inside the UCP TTL window (typically minutes to an hour depending on FreePBX config).
- Mitigation: operator hygiene (screen lock on step-away). Structural fix: signed
logout_ucpteardown endpoint + JSbeforeunload sendBeaconis queued for v1.0.1 so Switch-user / tab close proactively deletes the token.
Cryptographic primitives + threat assumptions
| Primitive | Used for | Assumed safe against |
|---|---|---|
| AES-256-GCM | Master-key encryption of *_ct columns |
Brute force, known attacks; AEAD provides tampering detection |
| HMAC-SHA256 | SSO landing token signing | Forgery without the secret |
| ed25519 | Per-PBX SSH keys | Key recovery without the private key; signature forgery |
| SHA-256 | Module sig hashes | Preimage / second-preimage attacks |
| bcrypt (PASSWORD_DEFAULT) | Admin password hashes | Offline dictionary attacks at well-chosen cost factor |
| GPG (RSA) | Module signing key 046E8CA0EE6A755B | Algorithm-level break (would catastrophically break Sangoma + every distro signing infrastructure too) |
random_bytes() |
Nonces, ed25519 keys, master keys | CSPRNG output unpredictability |
hash_equals() |
Token signature comparison | Timing-attack-based discovery of HMAC bits |
We do not roll our own crypto. Every primitive used is the language-standard implementation (openssl_*, random_bytes, password_*, OpenSSH's ssh-keygen, system GPG).
Known limitations
Things a fleet operator MUST know before deploying, ranked by likelihood of biting real users:
- HTTPS is mandatory end-to-end. Cross-origin iframe cookies over plain HTTP are blocked by every modern browser (RFC 6265bis). The Native UI and UCP-impersonation iframe features simply won't work in HTTP mode. Use self-signed certs during install; certbot on prod.
- SSO click for
variant=adminlands on the PBX as useradmin.Sso::DEFAULT_TARGET_USER = 'admin'is the fallback for the admin variant, so FreePBX-side audit showsadminfor anything a manager operator did through admin SSO. The v1.0.0 UCP variant is different — it always lands as the picked user, so UCP-side audit is per-user. Per-user attribution for the admin variant is still on the roadmap. - Native UI + UCP iframes reload on tab switch. No cross-tab DOM persistence. Every switch back mints a fresh SSO token + reloads the iframe. Working as intended given the "each tab is a page nav" architecture.
- UCP session teardown deferred to v1.0.1. After Switch-user / tab close, the row in
asterisk.ucp_sessionspersists until UCP's own TTL. IP-pinned to the operator's browser, but a walk-away workstation risk exists. A signedlogout_ucpteardown endpoint + JSbeforeunload sendBeaconis queued for v1.0.1. - Boot-token race window. Between
install.shprinting the setup URL and the operator claiming it, anyone with local read on/etc/customsso-manager/first-run.tokencould hijack the setup. Mitigation: claim the URL immediately after install, don't leave the box in setup-pending state. - Master key on disk alongside the encrypted data — limited defense against full-disk theft. See LUKS tradeoffs in SECURITY.md. Manager BACKUPS must include the master key or all secrets are unrecoverable.
- Root SSH password used exactly once at pair time. A network capture during the pair TCP session (SSH is encrypted, so this needs a broken SSH cipher, MITM against the manager's Paramiko host-key check, or shoulder surfing on the operator) could yield the pre-wipe password. In practice: seconds of exposure, then destroyed on the PBX.
- No source-IP restriction on the SSH authorized_keys line since v0.6.4. Dropped so DR-restore-to-a-different-IP works; host-key pinning (Paramiko
known_hosts) +no-pty,no-X11,…restrictions + AES-256-GCM-encrypted private key replace that defense. A leaked key alone is useless without the manager's master key + DB access. - No signed audit log — append-only by convention only. A root attacker on either host could doctor the audit table. Detect via off-host shipping (syslog forwarding, not bundled).
- The sidecar's SSH command allowlist for bulk operations is edited via the UI (Settings → Custom commands) — admins can define arbitrary shell templates. The templates run as root on every selected PBX. Treat the
adminrole as equivalent to fleet-wide root, per role docs. Operator-role users can only dispatch commands from the built-inCMD_MAP, not define new ones. - 2FA is email-code only. TOTP (authenticator apps) remains a roadmap item.
- Master-key rotation is single-transaction (best-effort atomicity). The re-encrypt runs inside one DB transaction; a mid-rotation crash rolls back cleanly. However, atomic two-phase rotation (concurrent old-key retention with a grace window for in-flight decrypts) is deferred (H10 audit finding). Rotate during a maintenance window.
Reporting issues
If you find a vulnerability — please don't open a public issue. Email signing@voip-stuff.net or DM via the project's communication channels.