8dd66945ddcc8ddf9cf0665b5eaa36b5146065df
56
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a1f09a0f7 |
Rename reinstall-mode prompt; make security-dashboard's "Full reinstall" a real teardown
Two-part change discussed and scoped in this session before touching
anything:
1. Rename "Reinstall in place" (r) -> "Update" (u) and "Full install" (f)
-> "Full reinstall" everywhere the prompt appears: lib/common.sh's
shared prompt_reinstall_mode(), plus the three services that carry
their own duplicated standalone-stub copy of it for standalone
execution (asterisk.sh, coturn.sh, wordpress.sh — per this repo's
documented standalone-bootstrap pattern). Internal state values
(update/fresh/cancel) are unchanged, so no other service's case
statement needed touching. docs/anveo-direct-setup-guide.md's `r`
reference updated to `u` to match. attic/asterisk-digital-ocean.sh
deliberately left alone — this repo's own policy is to not backport
fixes into attic/.
2. security-dashboard.sh's "Full reinstall" now does a real teardown
before reinstalling — stops and removes the systemd unit, sudoers
grant, Caddy site block, and secdash system user, then proceeds
through the normal fresh-install flow — instead of just overwriting
files in place while leaving the old service running underneath.
Prototype for a pattern discussed for other services later: split the
destructive question out explicitly ("also delete
dashboard-admins.conf — per-admin extension scoping?", default n) so
full reinstall doesn't silently discard state a plain "start over"
request wouldn't expect to lose. Verified the backup/restore mechanics
(mktemp, copy out before teardown, copy back after) against a mock
under `set -u` for both the preserve and wipe paths before shipping.
Update mode was already the strongest existing example of surfacing
newer optional prompts (its "Reconfigure Caddy protection?" /
"Reconfigure per-admin scoping?" sub-prompts already cover every setting
fresh-install offers) — no changes needed there for this service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
|
||
|
|
8c52376495 |
Remove stale Caddy block before rewriting it on a fresh security-dashboard reinstall
The fresh-install path called _secdash_configure_caddy directly with no prior removal, unlike the update/reconfigure path which already calls _secdash_remove_caddy_block first. Re-running a "Full install" over an existing dashboard on the same domain therefore appended a second site block instead of replacing the first — and since Caddy serves whichever block comes first in the file, the old one (old Authelia address, old Basic Auth settings) kept winning even after answering the prompts with new values. Confirmed live: reconfiguring a dashboard from a local to a remote Authelia address left the old forward_auth target still in effect until the stale block was deleted by hand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn |
||
|
|
c719197b20 |
Admin-scoping setup: show live extension list, auto-include owned DIDs
Two refinements to the per-admin extension scoping added last commit: - The setup prompt now shows the dashboard's current extensions (pulled from its own running /api/pstn-permissions, reusing list_extensions()'s already-correct pjsip.conf parsing instead of a second implementation in bash) before asking for each admin's list, with a real example built from actual extension numbers instead of a generic placeholder. Shown fresh for every admin added, one at a time. - An admin scoped to an extension now automatically sees that extension's directly-assigned personal DID's call/text history too, not just its internal activity — parse_pstn_calls()/parse_texts() key inbound rows by the DID that was dialed, not the owning extension, so without this a scoped admin would see their own extension's outbound calls but not inbound calls to their own number. New _dids_for_extensions()/ _admin_scope_for_calls() resolve this per-request from pstn-personal-dids.conf's direct (non-ring-group) owner field. Voicemail scoping is unaffected — a mailbox is always keyed by extension number regardless of which DID rang it. Also removed a dead DASHBOARD_ADMINS_HEADER Python constant left over from before the file-writing responsibility settled on the bash side only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn |
||
|
|
d124902b8d |
Add fail-closed per-admin extension scoping for Calls & Texts and Voicemail
Two admins sharing one dashboard can now each be scoped to their own extensions on the Calls & Texts and Voicemail tabs, while Security Log, CrowdSec, and Extensions stay fully visible to both — Authelia already provides real per-person identity here (Remote-User, forwarded by Caddy's existing forward_auth/import authelia wiring), this just teaches app.py to finally read it for these two tabs instead of ignoring it. New dashboard-admins.conf ([username] -> extensions=), configured via CLI prompts in security-dashboard.sh (offered at install and on reconfigure), read-only from app.py's side — no write access needed since the file is root-managed. allowed_extensions_for_user() is fail-closed by design: an empty/missing file means unrestricted (today's default, unchanged), but the moment one admin is configured, every other identity — an unlisted admin, a typo, or no Authelia identity at all — sees nothing on those two tabs until added. /voicemail/audio checks the same scope directly (not just the list route) so a guessed or copied URL can't bypass the filter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn |
||
|
|
8950810cea |
Add voicemail: dialplan/mailboxes in Asterisk, Extensions toggle + Voicemail tab with click-to-play in the dashboard
Asterisk side (services/asterisk.sh): a [voicemail-access] context reachable from every extension (*97 checks your own mailbox, *98<ext> drops a message into another mailbox directly), gated live via AST_CONFIG() on a new "voicemail" flag in pstn-permissions.conf. voicemail.conf gets a skeleton [general]+[default] at install/update, then stays dashboard-owned from there — mailbox lines are never regenerated wholesale by asterisk.sh once the file exists, matching every other install-time-vs-dashboard-owned file split in this repo (.env, firewall rules, etc). Vendor files (entrypoint.sh, easy-asterisk.sh) get patched the same way messaging-dialplan.conf already does, including the live-extensions.conf patch for boxes with existing devices. While tracing the right #include anchor for this, found and then reverted a theoretical "fix" to messaging's own #include position: pstn-trunk.sh's own comment (live-confirmed 2026-07-24) directly contradicts the textbook Asterisk #include semantics I'd assumed, so the safer move was keeping messaging's anchor exactly as already verified working and using the same position for voicemail's own #include. Dashboard side (services/security-dashboard.sh): write_voicemail()/ _apply_voicemail_flag() toggle the flag and a PIN (generated once, kept across future toggles), regenerate_voicemail_conf() keeps voicemail.conf's [default] section in sync, and a module reload takes effect without a full Asterisk restart. Extensions tab gets a Voicemail column next to Messaging, showing the PIN once generated. New Voicemail tab lists every mailbox's messages (parsed from Asterisk's own msgNNNN.txt sidecars) with an inline <audio> player per row — /voicemail/audio validates ext/msg against strict regexes plus a resolved-path containment check before ever opening a file. Dashboard gets read-only ACL + systemd ReadOnlyPaths access to the voicemail spool dir, and a new sudoers-scoped module-reload command. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn |
||
|
|
d4ca8277a6 |
security-dashboard: add Calls & Texts tab for PSTN calls and SIP/SMS texts
Surfaces pstn-trunk.sh's existing pstn-trunk-calls.log (already recording every PSTN call's numbers, just never shown on the dashboard) plus two new metadata-only logs: sip-messages.log for internal SIP MESSAGE deliveries/denials (asterisk.sh) and pstn-sms.log for SMS-over-SIP arrivals (pstn-trunk.sh). No message bodies are ever logged. The dashboard reads all three via a new /api/pstn-calls and /api/comms-texts pair, sharing a common bounded tail helper with the Security Log parser. |
||
|
|
9390382bf4 |
Add Sipnetic QR-scan provisioning and a kiosk client installer download
Two concrete asks: "a QR code generator for sipnetic... displayed on the security website" and a real download for the baresip kiosk client instead of just pointing at docs. Sipnetic QR: ea_device_sipnetic_string() builds Sipnetic's own documented account-string format (n=/u=/d=/p=/dt=, semicolon-separated, from https://www.sipnetic.com/qr-codes) from the exact same data the existing device-details panel already reads back out of pjsip.conf -- unlike ea_device_provisioning()'s deliberately generic plain-text file (written because no vendor XML format could be verified), this one has real documentation to build against. Rendered client-side with davidshimjs/qrcodejs (MIT, wraps Kazuhiko Arase's original QRCode for JavaScript) embedded verbatim with its license header intact -- no CDN call, no new Python dependency, same self-contained approach as the rest of this page. New "Sipnetic QR code" button on each extension's detail panel; verified the embedded library actually renders (not just parses) via a real jsdom run producing real QR cell output. Kiosk installer download: _secdash_copy_kiosk_installer copies vendor/easy-asterisk/easy-asterisk-v0.10.0.sh alongside the deployed app at install time (both update and fresh-install paths) and a new /download/kiosk-client-installer.sh route serves it -- linked directly from the Ring Groups card's help text instead of only being reachable by manually finding the file in the repo. Copied at install time rather than read live, since a standalone run of this one file (no full repo clone -- explicitly supported here) has no vendor/ directory to read from; missing source degrades to a clean 404 on that one link, not an install failure. Verified: bash -n, py_compile on the extracted embedded app.py, node --check on the extracted embedded JS (including the newly-embedded QR library), and the account-string output checked directly against Sipnetic's own documented example format. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww |
||
|
|
646b7b6fde |
Let an existing Ring Group's type/timeout be edited, not just set at creation
Rename, member add/remove, and DID assignment could all already be changed after a room existed -- type and timeout could only be set once, at creation, with no way to flip an existing Ring group to Page (or back) without deleting and recreating it, losing its members/DID assignment in the process. Reported directly: "I can't edit the ring group to change it to a page group." Turns the Timeout/Type columns into inline-editable controls (matching the same select/input the creation form already uses) with a Save button per row, backed by a new ea_update_room_settings() that rewrites just those two fields in rooms.conf, leaving name/members/DID untouched -- same read-modify-write pattern ea_rename_room() already uses. Verified: bash -n, py_compile on the extracted embedded app.py, node --check on the extracted embedded JS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww |
||
|
|
ca750f5432 |
Document Ring vs Page and auto-answer kiosks, in the dashboard itself
The user asked for the security webpage to actually explain how to set these up, not just this conversation. Adds a "How this works" disclosure to the Ring Groups card (matching the existing help-block pattern already used on Extensions and Personal numbers) covering: - Ring vs Page's actual difference (first-answer-wins hunt group vs Asterisk signaling auto-answer via SIP headers). - Mixing an auto-answering device with normally-ringing phones needs no new group type or per-member setting -- auto-answer lives in the device's own SIP client config, so a plain Ring group already dials everyone at once and an auto-answer-configured device just picks up faster than a human can. New docs/kiosk-paging-setup.md walks through the concrete path for a dedicated always-on auto-answer device: Easy Asterisk's own baresip-based "kiosk" client, installed on a separate small Linux machine (not the Asterisk server's Docker container -- the vendor script explicitly skips baresip install when it detects Docker locally). Also notes plainly that a browser/WebRTC auto-answer option doesn't exist on this server today (no WSS transport configured) and would be new infrastructure work, not a quick addition. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww |
||
|
|
eaca45c83c |
Read domain/TURN from Easy Asterisk's own config, and offer a settings download
The panel kept showing "no domain set" and "TURN not configured" on a box that has both. The cause was reading the wrong file: I had it reading the host-side .env, a 600 file needing an ACL grant, a ProtectSystem exception and a systemd Environment= line to even locate — three things that each had to be right, and weren't. The vendored web admin gets this right because it reads /etc/easy-asterisk/config, written by the container's own entrypoint. That is mounted on the host inside ASTERISK_EA_CONFIG_DIR, a directory this dashboard is already granted read access to for rooms.conf. So it now reads that first, falls back to .env, and finally to `docker exec cat` of the same file — each source only filling gaps the previous left. No new permissions, and the value shown is what Asterisk is actually running with rather than what the installer asked for. Also adds the requested provisioning file: a "Download settings" link per extension serving a plain text file with server, username, password, display name, transport, port, SRTP, a ready-made SIP URI, and TURN server/user/ password. Deliberately not a vendor-specific format — Sipnetic, Linphone, Zoiper and Groundwire each want a different one and none could be verified from here, and a confidently-wrong .xml is worse than a file you can read. It warns in-file when the extension is UDP-only, when the cert is self-signed, and that it contains a password. The panel gains the SIP URI and says when the address shown is the host IP rather than a configured domain. Fixes a JS syntax error introduced with that note: an apostrophe escaped for Python's benefit left a bare quote inside a single-quoted JS string, which broke the whole page script — the table rendered empty. Now checked properly by extracting the script and running `node --check` over it, which catches this class of fault directly instead of inferring it from missing elements. Verified with only Easy Asterisk's config present and .env absent entirely — the state that failed before: domain, TURN server, user and password all resolve, the download serves with the right filename, and the page has no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
1b3e7a6110 |
Fix extensions created from the dashboard, and show details by their row
Three separate faults, reported together as "the old web admin made extensions correctly and this doesn't". 1. Every write through ea_docker_write left pjsip.conf owned by root. `tee` runs as root inside the container while Asterisk runs as `asterisk` and expects to own its own config — the vendored admin's add_device() chowns it back immediately after writing, and services/asterisk.sh's device migration does too. This was the one writer in the project that didn't, and is the most likely reason an extension created here behaves differently from one created in the vendored admin. Now chowned after every write, with a scoped sudoers entry for it, and a warning logged if the chown itself fails rather than passing silently. 2. The dashboard's update path rewrote the systemd unit and then restarted the service without daemon-reload, so systemd kept running the cached unit. Any Environment= line added since the last FRESH install was written to disk and ignored — which is exactly how a box with DOMAIN_NAME and TURN both set in .env still reported "no domain set" and "TURN not configured". 3. The connection panel rendered at the top of the card, so on any table long enough to scroll, the answer appeared off-screen above the row that was clicked. It is now a table row injected directly beneath its own extension, toggled by the same button, and it survives the transport and password actions by reopening after the reload they trigger. Also: LAN devices now get ice_support=yes when a TURN server is configured, matching the vendored admin (a device given TURN credentials but no ICE can't use the relay); the panel reports the device type, so a Mobile extension can be confirmed as such; and an unreadable .env now says which of "path not set", "file missing", "permission denied" or "TURN_SERVER empty" applies instead of the flat "not configured" that covered all four. Verified: the three .env failure states each produce their own message; the detail row lands directly after its anchor, only one is ever open, it clears on re-render and reopens rather than sticking closed; no duplicate or missing element IDs and no JS errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
1f6c7cf349 |
Default new extensions to TLS, with transport behind an Advanced disclosure
Making the transport a visible, equally-weighted choice meant new users had to know the answer before they could get one right — and the wrong answer fails in the least diagnosable way available: a UDP-only endpoint doesn't refuse a TLS registration, it ignores it, so the phone times out and nothing is logged anywhere. The add form now creates Remote/FQDN (TLS 5061) extensions without asking. TLS is listed first in the markup so it is the default before any script runs, and the JS no longer switches it to LAN on a box with no domain — that box is not better served by UDP, it just needs the phone told to trust a self-signed certificate, which the disclosure now says at the point of choosing. Transport and auto-answer moved behind an "Advanced…" toggle, leaving name, extension and category as the whole form. The toggle resets on cancel and after a successful add so it doesn't stay open across uses. Verified in the browser on two fixtures — with and without DOMAIN_NAME set — that the panel starts hidden, the transport reads fqdn untouched in both cases, and each shows the right hint when opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
11f48ed970 |
Show connection details per extension, and let them be edited
Adding an extension from the dashboard gave back a password and nothing else — no server, no TURN credentials, and no indication of which transport the endpoint had actually been written with. A correctly-created extension and one that could never register looked identical. Each row gets an info button opening the full set: SIP server, username, password, transport and port, and the TURN server/user/password. The password is read back from pjsip.conf rather than regenerated, so re-pairing a handset no longer means deleting and recreating the extension. The same panel can switch an extension between LAN (UDP 5060) and Remote/FQDN (TLS 5061) and reset its password in place, keeping its category, room membership and PSTN permissions. That transport choice is the likeliest cause of a phone that looks right and never registers: an endpoint written transport=transport-udp will not answer a TLS registration and the phone just times out. The add form now defaults to Remote/FQDN whenever DOMAIN_NAME is set, rather than always LAN. Reading TURN details needs Asterisk's .env, which is chmod 600 — the installer now grants the service user read on that one file via ACL and lists it in the unit's ReadOnlyPaths, since ProtectSystem=strict would otherwise hide it. The UI says so plainly if the file still isn't readable. Also fixes a real gap: ea_add_device is a third, independent writer of endpoint blocks alongside the two vendor paths that services/asterisk.sh patches, and it was not emitting message_context=sip-messaging — so an extension created from this dashboard silently had no internal SIP messaging while one created from the vendor admin did. Verified against a fixture: transport round-trips TLS↔LAN without accumulating duplicate transport/media_encryption/ice_support keys, password reset rewrites only the target extension's auth section, delete still works after edits, and the browser shows the populated panel with the add form defaulting to fqdn when a domain is configured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
c02b908570 |
Fix the real cause of the persistent "1 extension edited" false positive
The previous fix (trimming the Name comparison) was real but not the
actual culprit here. loadExtensions()'s fallback for a device with no
pstn-permissions.conf entry — the normal, default state for any
extension never granted PSTN access — set restrict: "none", which
isn't one of the dropdown's five valid values (full/restricted/
restricted-in/restricted-out/internal). The <select> silently fell
back to its first option ("full") while the underlying model kept
"none", so the mismatch reappeared identically on every load
regardless of caching, matching a persistent bug survives hard
refresh and a private window. Use "internal" instead, matching the
documented default and the value the dropdown actually supports.
|
||
|
|
88b4510383 |
Fix persistent "1 extension edited" false positive on page load
rowEdits() compared the Name field's trimmed DOM value against the untrimmed model value — any device whose stored name has stray leading/trailing whitespace (e.g. from a manual pjsip.conf edit) permanently failed that comparison on every load, showing the unsaved-changes bar with nothing actually changed. Trim both sides. |
||
|
|
5d9f7dad4b |
Ring Groups: assign a personal DID right on the row, inherited by members
Adds a Personal number column directly on the Ring Groups table (same underlying write as the Personal Numbers card's owner picker, just surfaced where it's actually needed instead of requiring a separate trip) and closes the gap that group ownership previously left open: a DID assigned to a group only rang members on inbound calls, with no way for members to actually use it outbound. _reconcile_group_cid_members() now makes a Ring Group's DID double as every current member's outbound Caller-ID override in pstn-permissions.conf, wired into every path that can change either side of that relationship: assigning/reassigning/removing the group's DID, joining/leaving the group, renaming it (repoints pstn-personal-dids.conf's owner instead of leaving a stale reference, members' Caller-ID untouched since neither the DID nor membership changed), and deleting it (unassigns the DID entirely, clearing it from every remaining member). An individual extension's own personal_did always wins over a group's - a member only ever inherits the group's DID into an empty personal_did, and only ever loses an inherited one that still matches the group's current DID, so a real individual assignment is never clobbered. Verified end-to-end (create/assign/join/leave/rename/ delete, plus the individual-wins precedence case) against a local harness before committing, given how much the outbound Caller-ID path already burned this session. |
||
|
|
fa22779d61 |
Add member checkboxes to Ring Group creation
Rooms' original flow was always create-empty-then-add-members-one-at- a-time in the table afterward; the merge kept that flow and dropped Groups' actual "check boxes, name it, done" creation UX, which is what was actually asked for. Add-form now has a member picker so a Ring Group can be built with its full membership in one step; existing groups still use the per-row add/remove control for later changes. |
||
|
|
fafa9d9fee |
Merge Groups into Ring Groups (renamed from Rooms), drop standalone Groups
Rooms already had the one thing Groups lacked — a real, dialable Easy Asterisk extension with live ring/page dialplan logic — while Groups' only unique capability was personal-DID ownership. Rebuilding that ownership mechanism a second time under the Groups name would have meant duplicating Rooms' dialplan-generation code; instead Rooms (renamed "Ring Groups" for clarity) gains personal-DID ownership, and the standalone Groups card/API/backend is removed entirely. pstn-trunk.sh's existing group-owned-personal-DID machinery (pstn-personal-group-ring.sh, reading pstn-groups.conf by name) is untouched — a new sync_room_group_mirror() keeps that file in step with a Ring Group's live membership automatically on every create/rename/delete/member change, so pstn-trunk.sh never needs to learn rooms.conf's format. Room names now validate against the same pattern write_group() requires, so a room can never end up with a name that would silently fail to sync once assigned a DID. The PSTN-restart nudge now also fires on a Ring Group edit, but only when that specific group currently owns a personal DID — most Ring Groups never do, and prompting on every ordinary membership tweak would just be noise. "Auto-answer for everyone" is the existing Page (intercom) room type, not a new control. Messaging stays exactly what it already was: strictly per-extension, unrelated to Ring Group/personal-DID membership. |
||
|
|
a37cba242a |
Default the Security Dashboard to the Extensions tab
Security Log was the landing tab purely by markup order; Extensions is what actually gets used day to day. |
||
|
|
e9ff9b5112 |
Remove Categories management from dashboard, keep Rooms
Categories was a whole CRUD system (its own file, its own card, its
own API routes) for something that only ever had one functional
effect: tagging a device "mobile" to enable RTP NAT-keepalive tuning,
plus a per-category auto-answer default. Replaced with a single
"Mobile/cellular device" checkbox directly on the add-extension form
and each extension row, writing the same underlying category string
Easy Asterisk's device format already expects ("mobile" or
"standard") without a separate category registry to manage. Removes
ea_list_categories/ea_create_category/ea_delete_category/
ea_rename_category, their /api/ea-categories* routes, the Categories
card, and the now-unused categories.conf sudoers grant.
Rooms stays as-is — unlike Categories, it's the only actually-dialable
ring/page group in this dashboard (a real Easy Asterisk extension that
rings or pages members live), which the dashboard-only Groups feature
cannot replace.
|
||
|
|
c477d11eb7 |
Improve PSTN commit-changes UX, collapse Extensions, drop call-limit UI
The "Commit changes (restart Asterisk)" button was already wired
correctly, but lived in a passive banner that was easy to miss after
a save, letting an admin keep testing against Asterisk's stale
AST_CONFIG() read without realizing a restart was needed. Every
PSTN-affecting save now also prompts immediately ("Restart Asterisk
now?"), once per logical action (a single save, or one whole batch),
with the banner kept as a fallback for "not now".
Also:
- Extensions card is now collapsible like the other cards, open by
default.
- Concurrent-call caps card removed from the dashboard UI (the
underlying dialplan cap and pstn-limits.conf are untouched, just no
longer editable from this page).
- Categories/Rooms and Groups/Personal numbers are now visually
grouped under section headers, with a note clarifying that a Room
is a real dialable ring-group extension while a Group is a
dashboard-only bulk-action convenience, since both being "named
sets of extensions" invited exactly that confusion.
|
||
|
|
7baaae2b6e |
Name the PSTN modes as extensions of the original tiers
The behaviour was right but the vocabulary wasn't: the previous commit replaced full/restricted/internal with a new none/open/out/in/both naming, when the ask was to extend the existing tier dropdown rather than supplant it. The dropdown now reads: full, restricted (both ways), restricted incoming, restricted outgoing, internal. The stored values match — internal, restricted, full, restricted-in, restricted-out — so the original three keep their own names in the config file and migrating a legacy install is now near-identity for them (tier=full becomes restrict=full, tier=restricted becomes restrict=restricted, which is what that tier already meant). restricted incoming = dials anywhere, only whitelisted numbers get through. restricted outgoing = anyone can call in, may only dial the whitelist. Named in parallel rather than as "full incoming", so the two sit next to each other in the list without needing the parenthetical to tell them apart. No behavioural change: the derived tier_out/allowed_out/tier_in/allowed_in the dialplan reads are unchanged, so the dialplan itself is untouched. Verified: legacy migration mapping the original tiers to their same-named modes; both new modes round-tripping through the dashboard and compiling to the right derived keys (restricted-in sets allowed_in only, restricted-out sets allowed_out only); whitelist disabled on full and internal; and the group-ring helper still ringing restricted-incoming only for a whitelisted caller while restricted-outgoing rings for everyone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
e96df78ab5 |
Rework PSTN permissions to one whitelist plus a direction mode
The previous commit gave each extension two independent lists — numbers it may dial and caller IDs that may reach it. That was more than asked for: the whitelist is one set of numbers per extension, and what varies is which direction(s) it constrains. pstn-permissions.conf now has an authored pair, 'restrict' and 'allowed_numbers', where restrict is one of: none no PSTN at all open unrestricted both ways out may only dial the list in may only be called by the list both the list applies both ways tier_out/allowed_out/tier_in/allowed_in are now derived from that pair rather than authored directly, and remain what the dialplan reads — so the dialplan is unchanged from the previous commit and stays tested. 'tier' still mirrors tier_out for rollback. Keeping the compiled keys means the file has one place a human edits and one place Asterisk reads, which is the same authored/ compiled split a named-number-list feature would need later. The migration handles both prior shapes: a genuinely legacy single-tier file (full becomes open, restricted becomes both — reproducing what the old dialplan did), and the short-lived two-list shape from the previous commit (inferred back to a mode, preferring the more restrictive reading). Still idempotent, still backs up first. The dashboard drops from two dropdowns and two fields to one of each, with the whitelist greyed out for the modes that don't use one, and the PSTN column sorting by how much reach a mode grants rather than alphabetically. Verified: migration from both shapes; the dashboard round-trip writing restrict=in with the list compiled to allowed_in only; and the group-ring helper across all four modes — Restrict inbound rings only for a whitelisted caller, Restrict outbound rings for everyone, Restrict both rings only for its own list, No PSTN never rings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
5747ad7fda |
Split the PSTN permission tier into independent inbound and outbound axes
One tier and one allowed_numbers list governed both directions, with the same list matched against dialled numbers going out and caller IDs coming in. Those are different sets, so "dial anyone but only accept calls from a short list" — and its reverse — were not expressible at all. pstn-permissions.conf now carries tier_out/allowed_out and tier_in/allowed_in. The dialplan reads them in the four places that gate a call: the outbound NANP pattern, the international leg, each ring-group member block, and personal-DID inbound routing — plus the group-ring shell helper. Internal extension-to-extension calling and ring groups remain ungated by either, as before. Existing installs are migrated in place by _pstn_migrate_permissions_split, which copies the old single tier into both directions (reproducing the box's current behaviour exactly), backs the file up first, and is idempotent. It runs alongside the dialplan write rather than after a reload, since the new dialplan reading un-migrated keys would fail closed and deny every call. tier/allowed_numbers keep being written as a mirror of the outbound values so that rolling back to a pre-split pstn-trunk.sh degrades to the old semantics instead of breaking. The dashboard's Extensions table gains Outbound/Can-dial and Inbound/Can-be- called-by columns, each number field enabled only by its own tier, tier columns sorting by permissiveness rather than alphabetically, and a save that posts the whole permission record so an unchanged direction isn't reset. Verified: the migration on a hand-written config (idempotent on rerun, messaging and personal_did preserved); generated dialplan and group-ring helper reading the split keys; and the group-ring helper resolving a real three-extension case — out=full/in=restricted rings only for a whitelisted caller, out=restricted/in=full rings for everyone, in=internal never rings — plus the browser round-trip writing tier_out=full with tier_in=restricted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
e709413157 |
Send Cache-Control: no-store with the dashboard page
The UI is inlined in app.py, so an upgrade changes the HTML behind a URL that never changes and carried no cache headers. A browser holding the previous page after an update is indistinguishable from the update having failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
51d090ee2c |
Modernize the Extensions tab: batched saves, collapsed sections, inline edit
The tab consolidation fixed what the page showed but not how it read. With two extensions and nothing else configured it rendered ~1900px tall: six cards all expanded, three paragraphs of prose before the first control, eight Save buttons with no indication of which rows had been touched, browser prompt() dialogs for renames, and a nine-column table with no scroll container. Interaction: - Name and Category are edited in place and feed one batched save. Edited rows get a highlight and a left rail; a sticky bar reports the count and Save changes commits only those rows, routing each field to the endpoint it needs (rename/category through the container, tier/numbers/messaging through the permissions file — or the messaging-only endpoint with no trunk). Discard reverts; leaving with edits pending warns. Delete keeps its own per-row control since it is destructive. - Categories, Rooms, Groups, Caps and Personal numbers became <details> sections with item counts, so the tab opens on the extensions table. Explanations moved behind "what this means" disclosures. - Add-extension is behind a button; the one-time device password gets a persistent dismissible callout rather than a line that scrolls away. Everything else reports through toasts, replacing six inline message divs. Presentation: a token-based stylesheet (spacing/radius/colour scale), sticky translucent header, sticky table headers, status as a colour-coded pill, focus-visible outlines, primary/secondary/danger button hierarchy, and a .table-wrap that owns horizontal overflow. Security Log and CrowdSec cards picked up the same card-body padding and scroll wrappers. Verified in Chromium against a fixture Asterisk config at 1280px and 390px: full layout opens at ~700px tall with five collapsed sections, editing two rows marks both and shows "2 extensions edited", saving persists and clears the dirty state, per-extension failures are reported individually rather than swallowed, the bare no-container/no-trunk layout still collapses to Ext/Name/Messaging with two cards and saves via the messaging-only endpoint, and at 390px the page does not scroll horizontally while the table does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
bde7e9d12d |
Merge dashboard Asterisk Admin, Extensions and PSTN Trunk into one tab
Three tabs listed the same extensions three different ways: Asterisk Admin as devices with category/status/transport, Extensions as a row of messaging checkboxes, PSTN Trunk as permission tiers with a duplicate Messaging column that wrote the same flag. Changing one extension meant knowing which of the three owned the setting you wanted. There is now one Extensions tab with one extensions table, merged from pjsip.conf (via /api/pstn-permissions, which always works) and /api/ea-devices where the Easy Asterisk container is reachable, keyed by extension so a row known to only one source still shows. Capabilities add columns rather than nav buttons: Category/Status/Transport are .ea-only, Tier/Approved-numbers are .pstn-only, and both classes start on <body> so nothing flashes before /api/ea-status and /api/pstn-status answer. Categories, Rooms, Groups, Concurrent-call caps and Personal numbers are cards under the same tab, gated the same way. Per-row Save picks its write path: tier + approved numbers + messaging via /api/pstn-permissions with a trunk installed, messaging alone via /api/pstn-messaging without one — which is what that endpoint has always been for. No backend changes; the standalone messaging-chips card and the duplicate Messaging column are both gone. Verified in Chromium against a fixture Asterisk config: full layout renders nine columns and six cards, the bare layout collapses to Ext/Name/Messaging with two cards, and both save paths write pstn-permissions.conf correctly (messaging-only leaves tier and allowed_numbers untouched). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAddJGE1G6eGaPzmScG5Vh |
||
|
|
3727eb6951 |
security-dashboard: add a Commit Changes button to the PSTN Trunk tab
Confirmed live this session: writes to pstn-permissions.conf, pstn-groups.conf, and pstn-personal-dids.conf land on disk immediately, but AST_CONFIG() in the dialplan sometimes kept returning a stale value (e.g. a personal DID's old owner after reassigning it to a group) until the Asterisk container was fully restarted - not just dialplan/module reload. This contradicts the "live, no restart needed" premise the rest of this tab's copy relies on, so surfacing it explicitly beats letting admins discover it by trial and error. Adds a "Commit Changes (Restart Asterisk)" button and banner to the PSTN Trunk tab, shown after any save on that tab (permissions, limits, groups, personal DIDs) and warning via beforeunload if left uncommitted. Backend restart_asterisk_container() runs `docker restart` on the Easy Asterisk container through the same scoped-sudoers/run_sudo mechanism the native Easy Asterisk Admin tab already uses for its own docker exec calls, so this needed one new sudoers line, not a new privilege model. |
||
|
|
802b51b34d |
pstn-trunk: strip a leading + before normalizing Caller-ID/whitelist numbers
Live trace confirmed: once trust_id_inbound=yes started surfacing real caller identity (previous commit), Anveo delivers it "+E.164" style (e.g. "+15551234567") instead of the bare digits the old callerid-fallback path produced. PSTN_CALLERID_NORM's existing "add a leading 1 if length is 10" check never fires for a 12-character "+"-prefixed value and never strips the "+" either, so it can never match an 11-digit, digits-only allowed_numbers entry no matter how correctly the number is whitelisted - a correctly-configured restricted-tier extension or group member would still always get busy. Fixed in three places: the inbound dialplan's PSTN_CALLERID_NORM computation (new PSTN_CID_RAW step strips a leading "+" first), the group-ring shell script's own defensive re-normalization, and the dashboard's admin-input-side normalizers (_normalize_nanp_number, _normalize_personal_did_input) so pasting a number straight out of a phone's call log (which naturally includes the "+") works too instead of being silently dropped. |
||
|
|
8a99544ee1 |
Close remaining 10-vs-11-digit gaps in PSTN number input
Full audit of every phone-number comparison/storage point across pstn-trunk.sh, security-dashboard.sh, asterisk.sh, asterisk-digital-ocean.sh, and the vendored easy-asterisk base script, prompted by the inbound Caller-ID normalization fix — the same digit-count mismatch was also possible on the admin-input side, just silent instead of loud: - security-dashboard.sh's write_permission(): a 10-digit whitelist entry was silently DROPPED (NUMBER_RE required exactly 11 digits), with no warning unless every entry in the field was invalid — a mixed 10-digit + 11-digit list saved "successfully" while quietly losing the 10-digit one. Now normalizes any bare 10-digit token to 11-digit instead of discarding it (_normalize_nanp_number). - write_personal_did(): required exactly 10 digits, rejecting an 11-digit entry outright with a clear (but avoidable) error. Now accepts either and normalizes to the canonical 10-digit storage form (_normalize_personal_did_input). - pstn-trunk.sh's TRUNK_DID install prompt: same fix, strips a leading "1" instead of aborting the install over it. Everything else checked out clean: outbound dialed-number matching already normalizes via the existing _NXXNXXXXXX pattern (adds "1" before any tier check), the area-code/country-code international gates aren't phone numbers so digit-count doesn't apply, and asterisk.sh/ asterisk-digital-ocean.sh/the vendored base script have no phone-number comparison logic at all — this class of bug only lives in the PSTN permission/whitelist layer this project added on top. |
||
|
|
dd8d20cbc8 |
Fix secdash access to Asterisk config: use ACLs, not chown-fragile groups
Confirmed live on a real droplet: secdash could read pjsip.conf fine but /api/pstn-status kept returning false even though pstn-trunk-dialplan.conf genuinely existed. Root cause: the Asterisk container's own entrypoint runs `chown -R asterisk:asterisk /etc/asterisk` on every container start, and the numeric UID/GID it resolves to inside the container coincidentally collided with unrelated host system accounts (config/asterisk ended up owned by messagebus:uuidd on this box) - silently reverting whatever group membership _secdash_grant_asterisk_access had granted secdash at install time. This wasn't a one-time misconfiguration; it would have silently broken again on every future container restart. Switched the grant mechanism from chmod + usermod group membership to POSIX ACLs (setfacl) - chown doesn't touch ACL entries (only chmod recalculates the ACL mask, and nothing in this flow chmods after install), so the grant survives the container's own maintenance chown. Default ACLs (-d) also make newly-created files (a regenerated dialplan, a fresh personal-DID entry) inherit the same access automatically. Also added _secdash_grant_ancestor_traversal, which grants execute-only ACL traversal up the directory tree - needed on any box where DOCKER_DIR sits under a restrictive parent (e.g. /root on some cloud images defaults to 700, blocking a non-root secdash from ever reaching deeper directories no matter what those directories themselves grant). Falls back to the old chmod/group approach with a warning if the 'acl' package is somehow unavailable (it's installed automatically otherwise). Verified with a real non-root system test user against a fixture reproducing the exact failure (a leaf directory with no "other" access, owned by an unrelated user/group): confirmed blocked before the fix, confirmed read+write access after, and confirmed access survives a simulated `chown -R` (the container restart scenario) plus correct inheritance onto a freshly-created file afterward - all without re-running the grant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
6f5ed30469 |
Replace Caddy path-proxy with a fully native Asterisk Admin tab
Supersedes the previous commit's reverse-proxy approach entirely: instead of Caddy routing to Easy Asterisk's own separate vendored web admin process, the dashboard now reimplements that admin's functionality natively - one process, one page, real tab-switching, no separate app to proxy, patch, or embed. Reverts asterisk.sh/asterisk-digital-ocean.sh's WEBADMIN_BASE_PATH vendor patching and Caddy-skip logic back to their pre-proxy state (confirmed identical via diff) since neither is needed anymore. security-dashboard.sh additions: - ea_* functions covering full device/category/room parity with vendor/easy-asterisk/easy-asterisk-v0.10.0.sh's own web admin: list/add/ delete/rename/change-category for devices, list/create/delete/rename for categories, list/create/delete/rename/add-member/remove-member for rooms, plus live registered/unregistered status. Reads go straight through the host-side bind-mounted config files (same as the existing list_extensions() already does for pjsip.conf); writes go through `docker exec -i <container> tee <path>` instead of a direct host-side write, since Easy Asterisk's container writes these files as its own internal user and a host-side write would just be fighting that ownership again on the next container restart. - Found and fixed a real bug (inherited from the vendored admin's own template, not introduced here): a plain non-mobile LAN device leaves both the keepalive and ice template lines empty, producing two consecutive blank lines inside the endpoint's pjsip.conf stanza instead of one - which broke the delete/rename/category-change parsers' "blank line ends this device's block" boundary detection, leaving an orphaned tail of config behind on delete. Fixed by building the endpoint block from a filtered line list instead of positional template blanks. Confirmed via a full synthetic add/rename/category-change/delete cycle against realistic pjsip.conf/categories.conf/rooms.conf fixtures (with docker exec mocked to a local file) - round-trips back to the original fixture correctly. - New plumbing: _secdash_grant_asterisk_access grants read-only access to categories.conf/rooms.conf's directory (separate from pjsip.conf's, confirmed against the vendored source - /etc/easy-asterisk/*, not /etc/asterisk/*); _secdash_write_sudoers adds six exact (no wildcards) docker-exec sudoers entries scoped to the one Asterisk container actually installed, validated live with visudo -c; _secdash_write_systemd_unit passes the new ASTERISK_EA_CONFIG_DIR/ASTERISK_EA_CONTAINER env vars and adds the config dir to ReadOnlyPaths, validated live with systemd-analyze verify. - New UI: Asterisk Admin tab with Devices/Categories/Rooms cards, sortable tables matching the existing style, inline category-reassignment dropdowns, and per-room member chips with an inline add-member picker. Nav button visibility now checks live container reachability (/api/ea-status) instead of just Asterisk-install detection. Still unverified: the actual `docker exec` calls (module reload, dialplan rebuild, live status) against a real running Easy Asterisk container - only the file-parsing/transformation logic itself has been exercised, via mocked writes, not the real container plumbing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
d449586dde |
Replace the Asterisk Admin iframe with a native Caddy path-proxy
No more cross-origin iframe: the Security Dashboard now reverse-proxies
the real Asterisk web admin natively at /asterisk-admin/ on its own
domain via Caddy's handle_path, instead of embedding a separate site in
a frame. One domain, one login wall, for both.
- services/asterisk.sh / services/asterisk-digital-ocean.sh: patch the
vendored web admin's one hardcoded absolute API path
(`const API_BASE = '/api'`, confirmed via the real vendored source to
be the only absolute-path reference anywhere in its HTML/JS - no other
hrefs, no login-page redirect, plain HTTP Basic Auth instead) so it
resolves correctly when mounted under a sub-path, via a new
WEBADMIN_BASE_PATH env var threaded through entrypoint.sh. Verified
against the real vendored file: patched output is
'/asterisk-admin/api' with the env var set, unchanged '/api' without
it. Skip each service's own dedicated admin Caddy domain when the
Security Dashboard is already installed, since it fronts the admin
instead.
- services/security-dashboard.sh: _secdash_configure_caddy now accepts
the admin's port and Asterisk's own directory/domain, path-routes
/asterisk-admin/* alongside the dashboard's own handle{} block, and
writes WEB_ADMIN_BASE_PATH into Asterisk's .env + restarts that
container once proxying is confirmed live. Defaults the dashboard's
own domain prompt to the droplet's DOMAIN_NAME when detected, since
Caddy's SIP-TLS cert sync already depends on serving that exact
domain. Removed the old CSP frame-ancestors patching and the iframe
itself; the nav is now a plain link, shown only once the proxy is
confirmed wired up.
- Fixed _secdash_remove_caddy_block's marker match to tolerate the
dashboard's reverse_proxy line now living one indent level deeper
(inside its own handle{} block) - verified against a synthetic
Caddyfile that it still finds and removes exactly the right block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf
|
||
|
|
4f102b12c1 |
One dashboard to manage Asterisk: core tabs always on, optional tabs self-hide
Restructure the nav so it reflects what's actually installed on this box, letting one dashboard URL cover everything from a bare LAN Asterisk box up to a full droplet with a trunk and CrowdSec: - Security Log and a new Extensions tab (Groups + Internal SIP messaging, split out of the old "PSTN Trunk" tab) are always available - they only need Asterisk itself, not a trunk or CrowdSec. - Asterisk Admin, PSTN Trunk, and CrowdSec each check their own live install state on every page load and hide their own nav button entirely when not present, instead of showing an empty/placeholder tab. - Add crowdsec_installed() (checks for /usr/bin/cscli) and a /api/crowdsec-status endpoint, mirroring the existing pstn_installed()/ /api/pstn-status pattern. This fixes the earlier design where messaging/groups management lived inside the PSTN Trunk tab even though both work with plain Asterisk and no trunk at all - hiding that tab would have taken them down with it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
17a21c076f |
Dashboard UI cleanup: column sorting, iframe fix confirmed, messaging chips
- Security Log tab: replace the per-column text filter row with clickable sortable column headers (same pattern as the CrowdSec bans table). - PSTN Trunk tab: add sortable headers to the permissions, personal-DID (numeric on DID), and groups tables. - Move the "Internal SIP messaging" card to the bottom of the PSTN tab and replace its per-row table+Save-button layout with a checkbox chip row and a single Save-changes button. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
c893751859 |
Document group-owned personal number assignment
Update the dashboard's own Personal Numbers card description and the Anveo Direct setup guide to mention assigning a DID to a group instead of a single extension. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
dd3ea131ac |
Allow assigning a personal number to a group, not just a single extension
The dashboard's Personal Numbers card now accepts a group (stored as "@GroupName", unambiguous against a same-named numeric extension) as well as a plain extension. A group-owned DID rings every CURRENT member whose own tier/approved-numbers authorize the caller, computed fresh on every call by a generated pstn-personal-group-ring.sh (invoked via the dialplan's SHELL() function) rather than unrolled at install time, since group membership can change any time via the dashboard with no reinstall - unlike the shared ring-group, which is fixed at install/update time. Applies the identical per-member permission check the shared ring-group already bakes into the dialplan, just computed in a plain shell loop against the same two config files - group ownership doesn't bypass the tier/approved-numbers model. Tested standalone against mock config data (full/restricted/internal mix, matching/non-matching caller, empty and nonexistent groups) - all four cases behaved correctly. The dialplan's own SHELL() invocation is still unverified against a real call. Group ownership never touches pstn-permissions.conf's personal_did (outbound Caller-ID override) field, since there's no single extension to hang that on for a group. |
||
|
|
3ee3a45367 |
Fix personal-DID inbound lookup: 10-digit storage vs 11-digit EXTEN mismatch
Confirmed live: PSTN_PERSONAL_OWNER came back empty despite a real
assignment existing, because the Security Dashboard stores personal DIDs
as 10 digits (PERSONAL_DID_RE = ^\d{10}$) while Anveo's inbound INVITE
delivers the called number as 11-digit E.164 (with leading 1) in ${EXTEN}
- confirmed via PSTN_DID_CALLED=15557776655 in the live test. AST_CONFIG()
looking up an 11-digit key never found the 10-digit section.
Normalizes to 10 digits before the lookup (strips a leading digit only
when the string is actually 11 characters, so this doesn't misfire against
a provider that already sends 10). Also fixed the dashboard's own DID
input placeholder, which contradicted its own 10-digit validation error.
|
||
|
|
3661c8fbca |
Wire up internal SIP MESSAGE enforcement using a dedicated dialplan context
Confirmed against a live install's pjsip.conf/extensions.conf that every endpoint falls back to context=intercom for messaging (message_context blank), and that [intercom] owns one exact-match per-device dial pattern regenerated on every dialplan rebuild. Rather than risk racing that, every endpoint now gets message_context=sip-messaging (patched into both of Easy Asterisk's device-creation code paths, plus a one-time migration for existing devices), routing messages to their own [sip-messaging] context gated on the existing pstn-permissions.conf messaging flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
b10a626b22 |
Fix ntfy deny-all default, add crowdsec update mode, presence alerts, embedded Asterisk admin tab
- ntfy.sh: auth-default-access was deny-all, silently blocking both publish and anonymous subscribe on every topic once an auth-file is present. Default to read-write (same model as public ntfy.sh) so alerts from crowdsec.sh/pstn-trunk.sh actually get delivered. - crowdsec.sh: add update/fresh/cancel reinstall gating — reruns previously re-asked every optional prompt (ASN exempt, geo-allowlist, ntfy, remote LAPI) unconditionally with no way to just refresh in place. - asterisk-digital-ocean.sh: add optional ntfy alerts on extension registration going offline/online, checked every 2 minutes via systemd timer (cron.d fallback), offered on both fresh install and update. - security-dashboard.sh: replace the outbound-only Asterisk Web Admin link with an embedded, lazy-loaded iframe tab, with a best-effort Caddy frame-ancestors patch and an always-available "open in new tab" fallback. |
||
|
|
3359898b4e |
Add named extension groups and Security Log column filtering
Groups: a new always-available "Groups" card in the PSTN tab (same place as Internal SIP messaging, no dependency on a PSTN trunk being installed) lets you name a set of extensions and bulk-enable/disable messaging for all of them at once. Purely a management-layer convenience - pstn-groups.conf is never read by the dialplan, which only ever looks at per-extension keys in pstn-permissions.conf. Applying a group action just calls the same write_messaging() each individual checkbox uses, once per current member. Editing membership never retroactively changes anything already applied, and deleting a group never touches members' own settings - confirmed with tests covering create/apply/edit-membership/re-apply/delete. Security Log: added a per-column filter row (Time/Event/Account/Remote/ Severity), live as you type, filtering client-side against the already-fetched events rather than re-querying - persists correctly across the existing 30-second auto-refresh. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
7851e2befd |
Decouple internal SIP messaging management from PSTN trunk installation
Messaging has no dependency on a PSTN trunk existing (no cost, no carrier, no DID), but the whole PSTN Trunk tab - including the messaging checkbox added last commit - was hidden behind pstn_installed(), which only becomes true once services/pstn-trunk.sh's dialplan is actually wired in. That meant enabling messaging required going through full SIP trunk/provider setup first for no real reason. Added a standalone "Internal SIP messaging" card that's always visible in the tab regardless of trunk status, backed by a new write_messaging() that only touches the messaging key (leaving tier/allowed_numbers/ personal_did untouched) and creates pstn-permissions.conf from scratch if it doesn't exist yet. Confirmed the systemd unit's ReadWritePaths and the group/chmod grants already covered this - both are set up whenever a base Asterisk install is detected, independent of pstn-trunk - so no permission-layer changes were needed, only the dashboard's own artificial UI gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
8291eba55e |
Make the messaging permission flag live-editable via the dashboard
get_all_permissions()/write_permission() now handle messaging alongside tier/allowed_numbers as one save action, and the Security Dashboard's PSTN Trunk table gets a Messaging checkbox column - no more needing to re-run pstn-trunk.sh's CLI just to change who can use internal SIP texting, matching how tier/allowed_numbers/personal_did already worked. Tested that messaging correctly survives tier changes and personal-DID assignment/removal on the same extension (independent axes, as intended). Still explicitly not done, and said so in both READMEs rather than implying otherwise now that there's a nice UI for it: the actual SIP MESSAGE dialplan wiring that would make Asterisk enforce this flag. That gap hasn't changed - only the permission storage/UI layer around it has. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
fcb01a4d93 |
Add personal/per-extension DID assignment ("their own numbers")
Multiple DIDs can now share one trunk/account, each assigned to a specific extension - additive to the existing shared trunk DID/ring-group, which keeps working unchanged for everyone regardless of what's assigned here. - New pstn-personal-dids.conf (DID -> owner extension), read live by the dialplan for inbound routing: a call to a personal DID routes straight to its owner, checked against the owner's own tier/approved-numbers, no ring-group fallback since it's that extension's own line. - New personal_did= field per extension in pstn-permissions.conf: the outbound Caller-ID override, used by pstn_check_busy (the shared exit point for both domestic and international dialing) instead of the shared trunk DID when the calling extension has one assigned. - Both files kept in sync automatically by one write path - CLI prompt at install/update, or a new "Personal numbers" card in the Security Dashboard's PSTN Trunk tab - rather than requiring hand-editing both consistently. Reassigning a DID or giving an extension a new one cleanly drops the stale side of the old mapping. - Assigning a DID to an internal-tier extension is accepted but warned about (won't ring anyone until also granted full/restricted tier), matching this repo's permissive-with-warnings style rather than blocking the action outright. Also fixed a real bug found while building this: security-dashboard.sh's write_permission() did cp.remove_section(ext) whenever tier was set to "internal", silently discarding any messaging=yes or personal_did= already set on that extension. Now removes only the tier/allowed_numbers keys, dropping the section only once nothing else remains in it. Separately, de-anchored pstn-trunk.sh's install prompts from VoIP.ms as the implicit default (provider name default, server-hostname example wording, rate-prompt wording) now that Anveo Direct is an equally confirmed, tested provider option. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
616dcdcfbb |
Fix Security Log tab loading the entire multi-GB log into memory
parse_security_log() did f.readlines() on ASTERISK_LOG before slicing the last 5000 lines - that reads the ENTIRE file into memory first. That log is Asterisk's unrotated console/security output, and this tab polls it every 30 seconds from the browser. Confirmed live: on a 1GB-RAM droplet with a 1.4GB log file, this ballooned the dashboard (explicitly meant to be a lightweight stdlib-only process) to 677MB RSS / 1.8GB peak swap, which left CrowdSec unable to even start (boot timeout) and directly contributed to the droplet becoming unresponsive. Fixed by reading only a bounded ~2MB tail from the end of the file (seek + fixed-size read) instead of the whole thing - memory use is now constant regardless of how large the log grows. Tested against a 180MB synthetic log: memory delta dropped from being proportional to file size to ~7MB, in 0.05s. Also added log rotation (services/asterisk-digital-ocean.sh) for that same file, which had no rotation at all and reached 1.4GB in about 3 days - copytruncate avoids needing to signal the containerized Asterisk process to reopen its log handle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
78456e6a3a |
Fix ASN-exempt fork never actually disabling the hub-original scenarios
"cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum" was failing silently (stderr suppressed, "|| true" swallowed the non-zero exit) because these scenarios are members of the crowdsecurity/asterisk collection, and cscli refuses to remove/disable a collection member without --force. The un-exempted hub originals kept running side-by-side with the ASN-exempt local forks the entire time, independently banning the same traffic with no ASN awareness at all - confirmed live by a ban showing scenario "crowdsecurity/asterisk_bf" (the hub original) rather than "local/asterisk_bf" (the fork) for an ASN that had just been exempted. Fixed in two places: - crowdsec.sh's initial setup now passes --force and surfaces a clear warning (with the manual fix command) if it still fails, instead of silently continuing. - security-dashboard's set-asn-exempt.sh helper now re-asserts the forced disable on every ASN-list save, so an install already affected by this (from before this fix, or where the step failed for any other reason) self-heals the next time the ASN list is edited via the dashboard, without needing to re-run the full CrowdSec installer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
1f7f89b984 |
Fix ASN-exempt whitelist silently failing with Permission denied
set_asn_exempt() was writing /etc/crowdsec/scenarios/local-asterisk_*.yaml directly from the dashboard's own process, but those files are root:root mode 644 and the secdash service user (--shell /usr/sbin/nologin) has no write grant to them - confirmed by the reported "[Errno 13] Permission denied" error. Every whitelist/unban attempt through the dashboard was silently a no-op as far as CrowdSec was concerned, which is why a carrier's IP kept getting re-banned even after repeatedly trying to exempt its ASN. Fixed by routing the edit through a dedicated root-owned helper script (set-asn-exempt.sh, installed alongside app.py, root:root mode 700) that's the one thing added to the sudoers whitelist - same pattern every other CrowdSec-touching action here already uses (cscli via sudo), rather than loosening the scenario files' own permissions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
9b950808cc |
Add spend-cap kill-switch, international-calling toggle, per-minute checks
- Replace the hourly cron.d spend/volume check with a per-minute systemd timer (cron.d fallback), since it's now also the kill-switch's enforcement point and the interval is the exposure window. - Add a hard monthly spend-cap kill-switch: trips pstn-trunk-killswitch.conf (read live by the dialplan, both call directions, internal calling unaffected) once estimated spend reaches an admin-set cap, plus a loud ntfy warning at 80% of the cap before it trips. Does not auto-reset; clearing it is a CLI-only action (update mode) so a compromised/careless web session can't quietly re-enable spend after a trip. - Add a CLI-only (never web-dashboard) international-calling allow-list: continent/country menu, always asked every run with the resulting allow-list printed exactly once right after, optional auto-expiry with day-of and at-expiry ntfy notices, and active re-blocking once expired. Dialplan gates the US "011" prefix pattern on it for full-tier extensions. - Add an independent messaging_enabled flag per extension in pstn-permissions.conf for Asterisk's native SIP MESSAGE texting, since the risk profile (no cost) differs from PSTN calling permissions. The dialplan wiring to enforce it is flagged as a known gap pending live verification against Easy Asterisk's message routing, rather than shipping a guessed pattern. - security-dashboard.sh: detect whether pstn-trunk is actually installed (not just base Asterisk) and show a clear "not installed" state instead of misleading default caps/extensions; add clickable column-header sorting to the CrowdSec active-bans table. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho9mZgAkVpdz7S5wJkg8Nf |
||
|
|
876fd6553b |
Add inbound concurrency cap, bump defaults to 10/10, make caps live-editable
Adds an inbound concurrent-call cap mirroring the existing outbound one - outbound alone didn't protect against an inbound call-flood, which also costs money per-minute on VoIP.ms. Both defaults bumped from 3 to 10. Moves the cap numbers themselves out of static dialplan text and into a new pstn-limits.conf, read live via AST_CONFIG() the same way permission tiers already are - changing either cap takes effect on the next call, no Asterisk restart, no reinstall. "update in place" never touches this file, matching the existing protection for pstn-permissions.conf/.env/firewall/ Caddy config. Adds a concurrency-caps card to security-dashboard.sh's "PSTN Trunk" tab, above the existing permissions table, so both caps are visible and editable from the same web page. Tested against a real running instance of the Python app: default fallback when the file doesn't exist yet, save/persist, invalid-input rejection, and a bash-to-Python round trip on the generated file format. Inbound dialplan ordering mirrors outbound's existing pattern: permission check (is any ring-group member authorized for this caller) before the concurrency check, consistent with outbound's tier-check-then-busy-check order. |
||
|
|
3bd952e55d |
PSTN trunk: 3-tier live permissions + Security Dashboard web UI + dual target
Reworks the outbound permission model from a flat allow-list into three per-extension tiers (internal / restricted / full), addressing the ask for extensions that can only reach pre-approved numbers plus extensions with full US calling, while internal extension-to-extension dialing and ring groups stay ungated for everyone regardless of tier. Permissions now live in pstn-permissions.conf, read by the dialplan via Asterisk's AST_CONFIG() on every call instead of being baked into static dialplan text - editing that file takes effect on the next call, no Asterisk restart and no re-running the installer. "update in place" mode never touches this file (same protection this repo's update-mode convention already gives .env/firewall/Caddy config); only a "fresh" reinstall (with confirmation) or the web UI change it. Adds a "PSTN Trunk" tab to services/security-dashboard.sh: lists every extension (parsed from pjsip.conf) with its live tier and approved numbers, editable with no restart - this is what makes the tier model actually manageable day to day. Extracted the dashboard's systemd-unit writing into its own function so "update" mode refreshes it too (previously only fresh installs did), and generalized both the dashboard and the trunk service to detect either asterisk-digital-ocean or the home/LAN asterisk install. Inbound ring-group membership now checks each member's tier live per call via an unrolled per-member dialplan block (full always rings, restricted only if the caller's number is approved, internal never rings) rather than a single static Dial() string. Caught and fixed two real bugs during testing against a sandboxed vendor copy and a live instance of the (stdlib-only) Python dashboard app: - Asterisk Goto/GotoIf argument parsing: ring<ext>/skip<ext> are named priorities within the same extension (declared via "same => n(label),..."), not separate exten => entries, so jumping to them needs the single-argument Goto(label) form - the two-argument Goto(label,1) form used initially addresses a different, nonexistent extension named "label" instead. - A security-relevant REGEX() direction issue: the inbound Caller-ID check initially interpolated attacker-influenced call data into the PATTERN side of a REGEX() match rather than the tested-string side, which would let a crafted Caller-ID forge a match against an unrelated approved-numbers entry. Fixed by keeping the admin-controlled approved-list as the pattern and the live call data as the string being tested, consistently on both the outbound and inbound checks. Verified end-to-end: dialplan/pjsip generation and vendor-file patching (idempotent, syntax-checked) as before, plus the new permission-file round-trip between bash and Python, and the dashboard's new API endpoints exercised against a real running Python server (extension parsing, tier changes, number normalization, invalid-input rejection, atomic file writes). |