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 at v0.5+.
from=-restricted root shell on that PBX from the manager's IP. - Per-PBX HMAC secret — ability to mint valid SSO landing tokens for that PBX, becoming the configured admin 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
from="<manager_ip>",no-pty,no-X11,no-agent-forwarding,no-port-forwarding— only the pinned manager IP can log in with that key, and only for command execution, not interactive shell hijack - 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 secret - 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 + manager IP source, root shell on that PBX
- Defense:
from="<manager_ip>"restriction means the key only works from the manager's IP. Without the manager's network position, the key alone is useless. - What still works for them: if they ALSO control the manager IP (compromised manager host), full root on the PBX
- Mitigation: protect the manager host (see SECURITY.md #recommended deployment practices)
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)
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 feature simply won't work HTTP-mode. Use self-signed certs during install; certbot on prod.
- Every SSO click lands on the PBX as user
admin.Sso::DEFAULT_TARGET_USER = 'admin'is hardcoded. FreePBX-side audit showsadminfor anything a manager operator did through SSO. Fine for solo-admin fleets; a real problem for multi-tenant / multi-operator use. Per-user SSO attribution is on the roadmap. - Native UI iframe resets on tab switch. No cross-tab DOM persistence at v0.5. Every switch back to Native UI mints a fresh SSO token + reloads the iframe. Working as intended given the "each tab is a page nav" architecture. SPA-ify to fix.
- 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.
- Source-IP allowlist on the SSH authorized_keys line is single-IP (no CIDR, no multi-IP). A manager behind a load balancer or NAT change needs a re-pair or manual
authorized_keysedit. - 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 "admin" role as equivalent to fleet-wide root, per role docs.
- 2FA is email-code only. TOTP (authenticator apps) is v0.5+ candidate.
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.