- Industry
- Hosted PBX · VoIP infrastructure
- Product
- Security monitoring + operational containment
- Users
- Security operators, PBX admins, on-call engineers
- Stack
- NestJS · TypeScript · ESL · Redis · Postgres · Nodemailer
- Proof
- 4 sanitized evidence reconstructions · Jest + dated UAT notes
The problem
A compromised SIP credential, exposed route, or unsafe destination policy can become a stream of chargeable outbound calls. Without a centralized monitor, detection, containment, evidence, and recovery fragment across FreeSWITCH logs, firewall changes, and operator memory—while fraud continues.
Constraints
Delivery stayed inside the client's stack, tenancy, compliance and media-ownership boundaries. Where a choice was forced (suite vs owned plane, BYOC vs CPaaS, local vs cloud models), the architecture section below records the trade-off rather than a marketing rewrite.
Operators needed an immediate readable signal; administrators needed localhost APIs and a broker that could lock down gateways without handing the application general root access.
The solution
Four layers with explicit boundaries:
- Prevent — SIP digest, provider ACL, firewall, Lua/dialplan destination guards (independent of monitor health).
- Detect — ESL normalization, Redis velocity/concurrency windows, deterministic rules with named reasons and score cap 100.
- Contain — incident created before broker actions; allowlisted commands (reject, hangup, block IP, quarantine extension, lockdown).
- Recover — preflight + two-step unlock on loopback; carrier re-enable is a separate gated procedure.
Architecture & validation stages

flowchart TB
Prevent["Prevent: SIP ACL Lua dialplan"] --> FS["FreeSWITCH ESL"]
FS --> Nest["NestJS orchestrator"]
Nest --> Redis[("Redis windows")]
Nest --> PG[("PostgreSQL audit")]
Nest --> Broker["Allowlisted broker"]
Nest --> Outbox["Alert outbox"]
Outbox --> Email["Operator HTML email"]
API["127.0.0.1:8099 API"] --> Broker
Operator-facing alert (frontend)
There is no production graphical dashboard—the implemented “frontend” is responsive HTML + plain-text email with severity, evidence table, first-response checklist, localhost commands, and an explicit recovery warning. Dynamic values are HTML-escaped.

Localhost API evidence

GET /health exposes policy version, ESL heartbeat age, fraud stats, and broker status. Top-level ok mirrors the controller—it is not a substitute for inspecting action_broker_ok and heartbeat age.

Routes include GET /api/security/incidents, POST /api/security/lockdown, and POST /api/security/unlock requiring incident UUID, reason, and confirm=UNLOCK after preflight review.
Detection → containment journey
sequenceDiagram participant FS as FreeSWITCH participant OR as Orchestrator participant R as Redis participant DB as PostgreSQL participant AB as Broker participant AS as Alerts FS->>OR: CHANNEL_CREATE outbound OR->>DB: security_event OR->>R: velocity windows OR->>DB: fraud_incident OR->>AB: lockdown reject quarantine OR->>AS: HTML text outbox
Validation timeline (sanitized summary)
- July 2026 — fail-closed MVP validated with outbound gateway disabled; broker preflight and ESL heartbeat checks passed in controlled review.
- August 2026 — inbound UAT passed after correcting an inbound-carrier false positive; outbound matrix items remained pending in supplied evidence.
No attack-volume, savings, or production SMTP delivery metrics are claimed on this page. Screenshots are sanitized reconstructions, not live production captures.
Outcomes
- Unified operator alerting with internal PBX security operations—not ad-hoc Twilio-console archaeology.
- Explainable deterministic rules (anonymous outbound, velocity, concurrency, distinct destinations).
- Least-privilege broker boundary instead of application root access.
- Durable PostgreSQL incidents, actions, and alert outbox with Redis as expiring windows only.
- Recovery semantics that do not silently reopen billing paths on unlock.
FAQ — buyer & architecture questions
Ten common questions about this case study, fit, and engagement.