security: headers, phone ACL, PIN rules for PINs set from the phone

Headers (08 §2.4) on every response: CSP (connect-src also allows
WEBRTC_WS_URL's origin when set), nosniff, Referrer-Policy,
Permissions-Policy. The dark-mode bootstrap moved from an inline
<script> to /theme.js so script-src stays 'self'.

Phone ACL (08 §2.2): with sip.restrictPhonesToLocalNets, every desk
extension endpoint gets deny 0.0.0.0/0 and permit for each local and
extra net. The spec's snippet was wrong: contact_acl names acl.conf
sections, a pjsip type=acl is the global ACL (it would filter trunks),
and Contact-header rules let a NATed remote phone through. Browser
endpoints come through the reverse proxy and aren't filtered. Golden
case phone-acl. Settings → Security holds it, with the provisioning
switches.

PIN rules for PINs changed from the phone's voicemail menu, which went
around the api: externpasscheck = sp-vm-pincheck (in the Asterisk
image, same rules as isTrivialPin, a unit test keeps them in step) and
minpassword = 4. Accepted PINs reach the api through AMI
VoicemailPasswordChange and are stored; before, the next config write
silently put the old PIN back.

e2e m11-t1: no CSP violations across admin pages; Asterisk refuses a
REGISTER ("Not match Endpoint ACL") until the network is listed; from
the phone, 1111 is refused by the check script and 7391 is stored and
survives a later config write.

Task: M11-T1
This commit is contained in:
iamdoubz committed 2026-10-19 14:47:04 -05:00
1 parent 62939f0138
commit 65e777ab41
38 files changed
+950 -19

No files matched your search

+16 -7
View File
@@ -7,7 +7,7 @@
| SIP scanners / password guessing | "friendly-scanner", sipvicious hitting 5060 | Generated 24-char SIP passwords; no anonymous endpoint; fail2ban/CrowdSec on `security.log`; optional phone ACL (§2.2); firewall allow-list |
| **Toll fraud** (the expensive one) | Stolen SIP creds used to call premium international numbers | International route disabled by default; per-user outbound class; `max_channels`; voip.ms-side international block + balance limits (06 §1); forward targets checked against class |
| Web credential stuffing | Login brute force | argon2id, rate limits, audit log, TOTP for admins (M11) |
| Voicemail PIN guessing | Calling *98 and trying PINs | 6-digit default PIN, `maxlogins=3`, reject trivial PINs (`000000`, `123456`, the extension number repeated) |
| Voicemail PIN guessing | Calling *98 and trying PINs | 6-digit default PIN, `maxlogins=3`, reject trivial PINs (`000000`, `123456`, the extension number repeated). As built (M11-T1): also for PINs changed from the phone's voicemail menu: `externpasscheck = /usr/local/bin/sp-vm-pincheck` (asterisk image, same rules, a unit test keeps it in step with `isTrivialPin`), `minpassword = 4`; the api stores accepted ones from AMI `VoicemailPasswordChange` (`services/vmpin.ts`), otherwise the next config write would put the old PIN back |
| Provisioning leaks | Fetching `/prov/<mac>.cfg` to steal SIP passwords | Per-device Basic auth, 404 on unknown MAC, rate limit + temporary IP block, optional LAN-only |
| Dialplan injection | Name `x); System(...)` | Sanitizers (03 §2), no `System()` anywhere |
| SSRF via webhooks | Webhook to `http://127.0.0.1:5038` | Private IP block by default (04 §9) |
@@ -22,14 +22,20 @@
server IPs (from the voip.ms wiki list, **verify**) plus `sip.extraAllowedNets`, for setups with no remote phones.
### 2.2 Phone ACL (setting `sip.restrictPhonesToLocalNets`)
When true, the generator adds:
When true, the generator adds to every **desk** extension endpoint (not `-web`, not trunks):
```ini
[sp-phone-acl]
type = acl
contact_deny = 0.0.0.0/0.0.0.0
contact_permit = 192.168.0.0/16 ; each network.localNets + sip.extraAllowedNets
deny = 0.0.0.0/0.0.0.0
permit = 192.168.0.0/16 ; each network.localNets + sip.extraAllowedNets (deduplicated)
```
and `contact_acl = sp-phone-acl` on every extension endpoint (not trunks). Golden test required.
Golden case `phone-acl`; `scripts/e2e/m11-t1.mjs` shows Asterisk refusing a REGISTER ("Not match
Endpoint ACL") and accepting it once the network is listed.
Changed from the first draft (M11-T1): `contact_acl = sp-phone-acl` names a section of **acl.conf**,
not pjsip.conf, and a `type = acl` section in pjsip.conf is res_pjsip_acl's *global* ACL, which would
filter trunks too. Contact-header rules alone are also weak: a phone behind NAT at home advertises a
private address. Source-address `deny`/`permit` on the endpoint is what keeps remote phones out.
Browser phones arrive through the reverse proxy (their source is the proxy), so `-web` endpoints
aren't filtered. The setting is on Settings → Security, with the provisioning switches (07 §2).
### 2.3 fail2ban / CrowdSec
- `logger.conf` writes `security.log` (01 §3.3).
@@ -45,6 +51,9 @@ and `contact_acl = sp-phone-acl` on every extension endpoint (not trunks). Golde
- HSTS is set by the reverse proxy (Caddy does it by default; nginx example adds `add_header Strict-Transport-Security "max-age=31536000" always;`).
- `TRUST_PROXY=true` → Fastify `trustProxy: '127.0.0.1'` only.
- Admin TOTP (M11): RFC 6238, 30 s, 6 digits, ±1 window, 10 single-use recovery codes (stored hashed).
- As built (M11-T1): the headers come from an `onSend` hook on every response (UI, API, `/prov`);
`connect-src` also allows `WEBRTC_WS_URL`'s origin when it's set. The dark-mode bootstrap moved
from an inline `<script>` in `index.html` to `/theme.js`, so `script-src` stays `'self'`.
### 2.5 Files and permissions
- `/etc/asterisk/selfphoneng/*` mode 0640, owner app UID.