Available Now

FreePBX Device Lock Module

Pin every extension to its physical phone and stop credential-theft toll fraud. Bind each SIP extension to the User-Agent of the device it was provisioned on — stolen credentials become useless on any other phone, scanner, or softphone.

Device Lock admin overview table showing all extensions with current lock target, last-seen User-Agent, and violation count
Device Lock Overview — every extension's lock target, last-seen User-Agent, and violation history at a glance

Why This Exists

Standard PJSIP auth on a FreePBX system checks one thing: does the password match? Anyone with a leaked extension/secret pair — pulled from a backup, an old config file, a misconfigured provisioning portal, or a brute-forced REGISTER scan — can sign in from any IP, on any device, and start placing calls on your dime. By the time the CDR alert fires the damage is already done.

The standard advice (Fail2Ban, strong passwords, lock to source IP) works only for stationary endpoints. The moment you have a real-world mix of desk phones, mobile softphones, hot-desks, home-office workers on dynamic IPs, and remote agents on cellular, IP-based restrictions become impractical and password rotation becomes a support burden.

How Device Lock Helps

Every legitimate SIP phone identifies itself in the User-Agent header of every REGISTER it sends — Cisco/SPA525G2-7.6.2f, Yealink SIP-T46G 96.84.1.5, PolycomVVX-VVX_311, and so on. Pin the extension to that fingerprint and the credentials become useless on any other device. The attacker's Zoiper, Linphone, or custom scanner announces itself as something different and is shown the door.

Compatibility: Built and tested on FreePBX 16 and 17 with PJSIP. No chan_sip support (deprecated upstream).

How It Works

Two cooperating enforcement paths — the REGISTER itself is allowed, but the moment the attacker tries to use it, the call is dropped.

Step 1 — The phone sends a REGISTER

A real desk phone, mobile softphone, or attacker with stolen credentials sends a SIP REGISTER to FreePBX/PJSIP. The request carries a User-Agent header that identifies the device — e.g. Cisco/SPA525G2-7.6.2f for a legitimate phone or Zoiper/2.10 for an attacker's softphone.

Step 2 — AMI listener picks up the ContactStatus event

PJSIP emits a ContactStatus event for every successful registration. The Device Lock daemon, subscribed to AMI, reads the User-Agent off that event and compares it to the lock configured for the extension — either a per-extension lock or the system-wide fallback.

Step 3 — Match or violation

If the User-Agent matches, the daemon updates last_seen_ua and clears any prior violation — the extension is good to go. If it doesn't match, the daemon writes a violation record (extension number, offending UA, timestamp) and sets the VIOLATION flag in AstDB.

Step 4 — Outbound call attempt is gated

The REGISTER itself completes — if it didn't, the phone would hammer the PBX retrying every few seconds. The block happens later: as soon as the rogue device tries to place a call, the dialplan splice in macro-user-callerid checks the AstDB violation flag. If it's set, the call is dropped with a "no service" prompt before Dial() is ever reached, and the attempt is logged in the CDR for forensic review.

1. AMI Listener Daemon

Subscribes to Asterisk's ContactStatus events and inspects the User-Agent of every successful REGISTER in real time. Mismatches are logged to the violation table and flagged in AstDB.

2. Dialplan Gate

Spliced into macro-user-callerid so it fires on every outbound call from every extension. If the violation flag is set, the call is dropped with a "no service" tone before it reaches Dial().

The REGISTER itself is allowed to complete (otherwise the phone hammers your PBX retrying every few seconds). The block happens the instant the attacker tries to use the registration for anything.

Per-Extension Locking

A "Device Lock" tab appears on every Extension edit page. Three ways to set the lock.

Lock to THIS device only

Captures the exact User-Agent the phone is currently registered with (down to the firmware version) and pins the extension to it. Use this when you want a specific physical handset, not "any Cisco SPA303 anywhere."

Lock to a phone model

Pick a regex pattern from the shared Phone Library (e.g. Cisco/SPA303 matches any SPA303 regardless of firmware). Useful when you swap handsets between users but keep the model uniform.

Lock to a custom User-Agent

Drop in your own exact string or PCRE regex for unusual devices, branded firmware builds, or staged rollouts.

Per-extension Device Lock tab showing lock mode selector, current registration status, last-seen User-Agent, and violation history
The Device Lock tab on an extension — lock mode selector, live registration status, last-seen User-Agent, and violation log

System-Wide Fallback Whitelist

Per-extension locking is granular but tedious to set up on day one. The system-wide fallback fixes that: turn it on, pick a default phone model (say, the Cisco SPA525G2 your office is standardized on), and every extension that doesn't have its own lock automatically inherits that fingerprint.

Roll the fingerprint out broadly, then carve exceptions for the handful of extensions that use something different. Disable it again and unlocked extensions go back to accepting anything. Re-enable and every PBX extension's enforcement state is recomputed against current registrations in one pass.

Phone Model Library

Ships with known fingerprints for Cisco SPA series, Yealink T-series, Polycom VVX, Grandstream GXP, Linphone, MicroSIP, Zoiper, 3CXPhone, and Asterisk PBX. Add new models as you encounter them — the "+ Add Model" button in the Overview table promotes any extension's currently-seen User-Agent directly into the library.

Deletion is guarded: an entry can't be removed while any extension is locked to its pattern, and the system-wide fallback won't let you delete its target while active. Tooltips explain exactly which extension or setting is blocking the delete.

Enforcement & Violation Logging

Every Registration Recorded

The listener writes last_seen_ua and timestamp for every extension. Live status banners on the per-extension tab show the current registration state and, when applicable, flag an active violation in red with the offending UA.

Violation Log

A separate violation log captures who tried what, when: extension number, the offending User-Agent, the timestamp, and a running violation count. When enforcement is active, the dialplan plays a "no service" prompt and hangs up. The CDR records the attempt for forensic review.

Under the Hood

AMI Listener Daemon

Runs under the asterisk user, watchdogged by cron with flock so a crash auto-recovers within 60 seconds. Lock file and log live under /var/run/asterisk/ and /var/log/asterisk/.

AstDB Storage

Stores per-extension lock state and violation flag at CUSTOMLOCK/LOCK_MODE/<ext> and CUSTOMLOCK/VIOLATION/<ext>. The dialplan check reads these directly — no MySQL hit on the call path.

Dialplan Splice

Uses FreePBX's doDialplanHook and reads the calling extension from ${CHANNEL} (not ${AMPUSER}, which is unset at the splice priority).

Database Schema

Three tables: customlock_phones (the library), customlock_extension (per-ext state, last-seen, violation history), and customlock_config (module settings).

Note on PJSIP identifiers: reordering the global PJSIP identifier chain (using match_header=User-Agent as a primary identifier) was considered and removed. Standard PJSIP identifies endpoints by an OR-chain, so a match_header block can't actually block a wrong-UA REGISTER unless you also drop the username identifier globally — which breaks every PBX endpoint that doesn't have a customlock identify block. The current AMI+dialplan design achieves the same effective result without touching the global PJSIP identifier chain.

Installation

Install as a standard FreePBX module. The AMI listener daemon and cron watchdog are installed automatically.

From Module Admin

Download the module

Download the tarball:

Upload in Module Admin

Go to Admin → Module Admin → Upload modules and upload the tarball. Or use the Download (From Web) option to pull it directly from the repo.

Install and Apply Config

Find "Device Lock" in the module list, click Install, then Apply Config. The module self-signs on install and registers a voip-stuff.net repository address for future updates directly through FreePBX Module Admin.

Command Line

fwconsole ma downloadinstall customlock
fwconsole reload

After installation, navigate to the Device Lock admin page to manage the Phone Library, toggle the System-Wide Fallback, and review the Extension Lock Overview.

Uninstall removes the cron entry, the listener, the AstDB tree, the dialplan splice, and the database tables — no manual cleanup needed.

Configuration

Two surfaces. Defaults are sane out of the box — no other settings to tune.

Per-Extension Device Lock Tab

  • Lock source — library / captured / custom
  • Live registration status
  • Last-seen User-Agent
  • Violation history

Device Lock Admin Page

  • Phone Library CRUD (add, edit, delete fingerprints)
  • System-Wide Fallback toggle and default model picker
  • Extension Lock Overview table with clickable extension numbers, current mode badge, library-match indicator, and "+ Add Model" shortcut

Frequently Asked Questions

Will Device Lock block a legitimate phone if its firmware updates?

It depends on the lock mode. "Lock to THIS device only" captures the exact User-Agent including the firmware version, so a firmware update will trigger a violation until you re-capture. "Lock to a phone model" uses a regex like Cisco/SPA303 that matches any firmware version — the recommended mode if you keep firmware up to date.

What happens if an attacker spoofs the User-Agent header?

Spoofing the User-Agent string requires the attacker to know exactly which device fingerprint is allowed for that extension. They can't get that from your PBX over the network — it lives in the FreePBX database. Combined with rate-limited REGISTER attempts, the attacker has to guess the fingerprint blind, which is dramatically harder than guessing a password. It's not absolute protection, but it removes the easy attack path.

Does it work with chan_sip?

No. Device Lock is PJSIP-only. chan_sip is deprecated upstream in Asterisk and FreePBX, and the module relies on PJSIP's ContactStatus AMI events.

Will it slow down call setup?

No. The dialplan check reads the lock state from AstDB, not MySQL — AstDB lookups are sub-millisecond. The User-Agent comparison happens asynchronously in the AMI listener daemon, not on the call path.

Can I temporarily disable enforcement for an extension?

Yes. Set the per-extension lock mode to "None" or clear the violation flag from the Device Lock tab. The system-wide fallback also has an off switch if you need to disable enforcement system-wide while troubleshooting.

Is the module free?

Yes. Released under the MIT license — free for commercial and personal use. No license keys, no subscriptions, no phone-home.

Get Device Lock

Download the module and stop credential-theft toll fraud on your FreePBX system in minutes.