diff --git a/docs/pstn-calling-voipms-plan.md b/docs/pstn-calling-voipms-plan.md index 667da84..3d68c6a 100644 --- a/docs/pstn-calling-voipms-plan.md +++ b/docs/pstn-calling-voipms-plan.md @@ -39,7 +39,7 @@ hardcoded to it — any provider supporting IP authentication works. Covers: - A configurable **inbound ring-group** (one extension or several), each member's live tier/approved-numbers checked per inbound call via an unrolled per-member dialplan block (no AGI needed). -- **`services/security-dashboard.sh` integration** — a "PSTN Trunk" tab +- **`services/security-dashboard.sh` integration** — its Extensions tab shows both concurrency caps and every extension (parsed from `pjsip.conf`) with its live tier and approved numbers, all editable with no restart. This is what makes the tier model and caps actually @@ -212,7 +212,7 @@ separately from that hourly check. admin-controlled approved-list in the pattern position and the live call data in the tested-string position — worth keeping that direction if this is ever refactored. -- **Web UI — implemented.** `services/security-dashboard.sh`'s "PSTN Trunk" +- **Web UI — implemented.** `services/security-dashboard.sh`'s Extensions tab lists every extension (parsed from `pjsip.conf`, the same marker format Easy Asterisk's own `rebuild_dialplan()` uses) with a tier dropdown and approved-numbers field, saving straight to `pstn-permissions.conf`. diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 7e8d565..eeab1bc 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -2091,7 +2091,7 @@ access specifically: Stored in \`config/asterisk/pstn-permissions.conf\`, read **live** by the dialplan via Asterisk's \`AST_CONFIG()\` on every call — editing this file -(by hand, or via the Security Dashboard's "PSTN Trunk" tab, if that service +(by hand, or via the Security Dashboard's Extensions tab, if that service is installed) takes effect on the next call, no restart needed. Re-running this installer in "update" mode never touches this file — only a "fresh" reinstall (with confirmation) or the web UI change it, the same protection @@ -2243,11 +2243,11 @@ Stored in \`config/asterisk/pstn-personal-dids.conf\` (DID -> owner, read live by the dialplan for inbound routing) and a \`personal_did=\` field per extension in \`pstn-permissions.conf\` (the outbound Caller-ID override) — both kept in sync automatically by the CLI installer and the Security -Dashboard's "PSTN Trunk" tab, live, no restart needed. +Dashboard's Extensions tab, live, no restart needed. ## Managing this from a web UI -If \`services/security-dashboard.sh\` is installed, its "PSTN Trunk" tab +If \`services/security-dashboard.sh\` is installed, its Extensions tab shows the per-extension permission tiers, the outbound/inbound concurrency caps, and personal-number assignments, all editable live — no restart, no reinstall. Install/update it any time with \`sudo ./setup.sh diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 0438726..a524027 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -64,16 +64,17 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then fi # ───────────────────────────────────────────────────────────────────────────── -register_service security-dashboard homelab "Security dashboard: Asterisk failed-connections + CrowdSec bans (Authelia-protected)" 8092 +register_service security-dashboard homelab "Security dashboard: Asterisk failed-connections + extension/trunk management + CrowdSec bans (Authelia-protected)" 8092 install_security-dashboard() { local APP_DIR="/opt/security-dashboard" local DASHBOARD_PORT=8092 local SVC_USER="secdash" - # Either Asterisk flavor works — prefer asterisk-digital-ocean if both - # happen to be installed, matching services/pstn-trunk.sh's own - # preference order for consistency. + # Either install layout works — prefer ~/docker/asterisk-digital-ocean + # (a droplet from before that service was merged into `asterisk`) if both + # happen to exist, matching services/pstn-trunk.sh's own preference order + # for consistency. local ASTERISK_EA_DIR="" if [ -d "$DOCKER_DIR/asterisk-digital-ocean" ]; then ASTERISK_EA_DIR="$DOCKER_DIR/asterisk-digital-ocean" @@ -86,9 +87,10 @@ install_security-dashboard() { # pjsip.conf — see vendor/easy-asterisk/easy-asterisk-v0.10.0.sh's own # CATEGORIES_FILE/ROOMS_FILE constants (/etc/easy-asterisk/*, not # /etc/asterisk/*). ASTERISK_EA_CONTAINER names the actual container to - # `docker exec` into for the native Asterisk Admin tab's writes/CLI - # calls (ea_* functions) — "easy-asterisk-do" for the droplet flavor, - # "easy-asterisk" for LAN, matching each service's own container_name. + # `docker exec` into for the Extensions tab's device writes/CLI calls + # (ea_* functions) — "easy-asterisk", or "easy-asterisk-do" for a droplet + # set up before the two Asterisk services merged, matching whichever + # container_name services/asterisk.sh actually used there. local ASTERISK_EA_CONFIG_DIR="${ASTERISK_EA_DIR:+$ASTERISK_EA_DIR/config/easy-asterisk}" local ASTERISK_EA_CONTAINER="" if [[ "$ASTERISK_EA_DIR" == *asterisk-digital-ocean ]]; then @@ -100,17 +102,17 @@ install_security-dashboard() { echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ SECURITY DASHBOARD │" - echo "│ Asterisk failed-connection log + CrowdSec decisions + PSTN │" - echo "│ trunk permissions, one page. Runs natively on the host (not │" - echo "│ Docker) so it can call cscli and read Asterisk's files │" - echo "│ directly. Authelia-protected. │" + echo "│ Asterisk failed-connection log + one Extensions tab (devices, │" + echo "│ categories, rooms, groups, PSTN tiers, DIDs) + CrowdSec bans, │" + echo "│ one page. Runs natively on the host (not Docker) so it can call │" + echo "│ cscli and read Asterisk's files directly. Authelia-protected. │" echo "└─────────────────────────────────────────────────────────────────┘" echo "" if [ -z "$ASTERISK_EA_DIR" ]; then - log_warning "No asterisk-digital-ocean or asterisk install detected." - log_warning "The Security Log, Extensions, Asterisk Admin, and PSTN Trunk tabs will just be" - log_warning "empty/hidden — CrowdSec's tab still works fine." + log_warning "No Asterisk install detected." + log_warning "The Security Log and Extensions tabs will just be empty — CrowdSec's tab" + log_warning "still works fine." fi if [ "$DRY_RUN" = true ]; then @@ -118,7 +120,7 @@ install_security-dashboard() { echo "[DRY-RUN] Would write $APP_DIR/app.py" echo "[DRY-RUN] Would write /etc/sudoers.d/security-dashboard (scoped cscli/systemctl/set-asn-exempt.sh only)" echo "[DRY-RUN] Would write a systemd unit and start it on 0.0.0.0:$DASHBOARD_PORT (firewalled via UFW, not interface binding)" - echo "[DRY-RUN] Would grant read/write access to the detected Asterisk config dir (for the PSTN Trunk tab)" + echo "[DRY-RUN] Would grant read/write access to the detected Asterisk config dir (for the Extensions tab)" echo "[DRY-RUN] Would configure Caddy + Authelia for a domain you'll be prompted for" return 0 fi @@ -202,59 +204,66 @@ protected page. Runs natively on the host (systemd service \`security-dashboard\ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly. ## Tabs -The nav only ever shows tabs for things actually present on this box — no -tab for a service you haven't installed. **Security Log** and **Extensions** -are always there (they only need Asterisk itself, detected once at install -time). **Asterisk Admin**, **PSTN Trunk**, and **CrowdSec** each check their -own live install state on every page load and hide their own nav button -entirely if not found, so this one page/URL scales from a bare LAN Asterisk -box (just those first two tabs) up to a full droplet with a trunk and -CrowdSec, without ever showing a tab for something that isn't set up. + +Three tabs: **Security Log**, **Extensions**, **CrowdSec**. The first two are +always there (they only need Asterisk itself, detected once at install time); +CrowdSec checks its own live install state on every page load and hides its +nav button if \`cscli\` isn't found. + +Extensions used to be three separate tabs — *Asterisk Admin*, *Extensions* +and *PSTN Trunk* — which between them listed the same extensions three times: +once as devices with a category/status, once as a row of messaging +checkboxes, and once as permission tiers. They're now one tab with one +extensions table, and each capability adds columns and cards to it instead of +a nav button of its own. That means the page still scales from a bare LAN +Asterisk box up to a full droplet with a trunk, without ever showing a +control for something that isn't set up — you just don't have to remember +which tab a given extension's settings live on. - **Security Log** — parses \`$ASTERISK_LOG_DIR/full\` for SIP auth failures (wrong password, unknown extension, etc.) with timestamp/account/remote IP, sortable per column (click a header to sort, click again to reverse). -- **Asterisk Admin** — a native reimplementation of Easy Asterisk's own - vendored web admin (\`vendor/easy-asterisk/easy-asterisk-v0.10.0.sh\`'s - device/category/room management), not a link or an iframe to that separate - process — one page, one login. Reads \`pjsip.conf\`/\`categories.conf\`/ - \`rooms.conf\` directly (same formats the vendor's own - \`easy-asterisk --rebuild-dialplan\` CLI still generates the dialplan from); - writes go through \`docker exec ... tee\` (root, sudo-gated) instead of a - direct host-side file write, since Easy Asterisk's container writes these - as its own internal user and a host-side write would just be fighting that - ownership again on the next restart. Its nav button only appears once the - live \`/api/ea-status\` check confirms an Asterisk container is actually - reachable. - - **Devices** — add/rename/delete a SIP extension, reassign its category; - live registered/unregistered status per device. +- **Extensions** — one row per extension, merged from \`pjsip.conf\` (which + always works) and, when the Easy Asterisk container is reachable, its own + device list. Columns: Ext, Name, then Category/Status/Transport if that + container is present, then Tier/Approved-numbers if a PSTN trunk dialplan + is installed, then Messaging (always — internal SIP texting has no PSTN + dependency at all: no cost, no carrier, no DID) and a per-row Save. Save + writes tier + approved numbers + messaging together when there's a trunk, + and messaging alone when there isn't. + - **Extensions** — add/rename/delete a SIP extension, reassign its category; + live registered/unregistered status per device. This is a native + reimplementation of Easy Asterisk's own vendored web admin + (\`vendor/easy-asterisk/easy-asterisk-v0.10.0.sh\`'s device/category/room + management), not a link or an iframe to that separate process — one page, + one login. Reads \`pjsip.conf\`/\`categories.conf\`/\`rooms.conf\` directly + (same formats the vendor's own \`easy-asterisk --rebuild-dialplan\` CLI + still generates the dialplan from); writes go through + \`docker exec ... tee\` (root, sudo-gated) instead of a direct host-side + file write, since Easy Asterisk's container writes these as its own + internal user and a host-side write would just be fighting that ownership + again on the next restart. Every write reloads PJSIP and/or rebuilds the + dialplan automatically, the same way the vendored admin's own actions do. - **Categories** — device profiles (an auto-answer default + description). - **Rooms** — ring groups/paging groups; add/remove members per room. - - Every write reloads PJSIP and/or rebuilds the dialplan automatically, the - same way the vendored admin's own actions do. -- **Extensions** — always available, independent of any PSTN trunk. A - **Groups** card lets you name a set of extensions and bulk-enable/disable - messaging for all of them at once — a management convenience only, not a - runtime concept: applying an action just writes the same per-extension - \`pstn-permissions.conf\` key each member's own checkbox would, and - membership changes never retroactively affect anything already applied. - An **Internal SIP messaging** card (a checkbox chip per known extension, - independent of PSTN calling entirely — no cost, no carrier, no DID, no - dependency on a PSTN trunk being installed) sits below it. -- **PSTN Trunk** — its nav button only appears once - \`services/pstn-trunk.sh\`'s dialplan is actually installed - (\`pstn-trunk-dialplan.conf\` present), so it never shows a - real-looking-but-unenforced editor. When present: the outbound/inbound - concurrent-call caps, and every known extension's permission tier - (internal / restricted / full) and, for restricted, its approved numbers — - all editable live, no Asterisk restart, no reinstall, sortable per column. - Also manages personal-number assignments (DID -> owner extension or - group), additive to the shared trunk DID. Writes directly to - \`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`, - which the dialplan reads fresh on every call. The spend-cap kill-switch - and international-calling allow-list are deliberately **not** managed - here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more - security-sensitive than what this tab already exposes. + - **Groups** — name a set of extensions and bulk-enable/disable messaging + for all of them at once. A management convenience only, not a runtime + concept: applying an action just writes the same per-extension + \`pstn-permissions.conf\` key each member's own checkbox would, and + membership changes never retroactively affect anything already applied. + (A group owning a personal DID *is* evaluated live against current + membership, though — see below.) + - **Concurrent-call caps** and **Personal numbers** appear only once + \`services/pstn-trunk.sh\`'s dialplan is actually installed + (\`pstn-trunk-dialplan.conf\` present), so the page never shows a + real-looking-but-unenforced editor. Caps are the outbound/inbound + concurrent-call limits; personal numbers map a DID to an owner extension + or group, additive to the shared trunk DID. Writes go directly to + \`pstn-limits.conf\` / \`pstn-permissions.conf\` / \`pstn-personal-dids.conf\`, + which the dialplan reads fresh on every call. The spend-cap kill-switch + and international-calling allow-list are deliberately **not** managed + here — CLI-only, via \`sudo ./setup.sh pstn-trunk\` — since both are more + security-sensitive than what this tab already exposes. - **CrowdSec** — its nav button only appears once \`cscli\` is detected on this host. Current bans (\`cscli decisions list\`), a delete/unban button per entry, carrier/ASN + country columns (sortable per column), and @@ -289,8 +298,8 @@ sudo journalctl -u security-dashboard -f \`systemctl restart crowdsec\`, and \`set-asn-exempt.sh\` (root:root, mode 700, installed alongside \`app.py\` — the one thing that edits CrowdSec's Asterisk-scenario YAMLs, since \`secdash\` has no write access to those - root-owned files directly and shouldn't). Asterisk Admin (only added if an - Asterisk install is detected): \`docker exec -i tee\` against + root-owned files directly and shouldn't). Extension/device management (only + added if an Asterisk install is detected): \`docker exec -i tee\` against exactly \`pjsip.conf\`/\`categories.conf\`/\`rooms.conf\`, plus \`asterisk -rx "module reload res_pjsip.so"\`, \`asterisk -rx "pjsip show endpoints"\`, and @@ -386,7 +395,7 @@ _secdash_grant_asterisk_access() { # fresh temp file then renames it into place) — only on the config dir, # not the log dir (no reason for secdash to ever create files there). # _ea_config_dir (categories.conf/rooms.conf) deliberately stays - # read-only — the native Asterisk Admin tab writes those through + # read-only — the Extensions tab writes those through # `docker exec ... tee` instead (see the ea_* functions), not a direct # host-side write, so there's no reason to grant it write access at all. if [ -n "$_config_dir" ] && [ -d "$_config_dir" ]; then @@ -454,13 +463,13 @@ SDSVC _secdash_write_sudoers() { local _svc_user="$1" _ea_container="${2:-}" local _ea_lines="" - # Native Asterisk Admin tab (ea_* functions) — every write goes through + # Extensions tab device management (ea_* functions) — every write goes through # `docker exec -i tee ` instead of a direct # host-side file write (see _secdash_grant_asterisk_access's comment on # why), plus the two Asterisk CLI calls needed after a change and the # live registration-status check. All seven are exact commands, no # wildcards, scoped to the one container actually installed on this box. - # The last line (docker restart) backs the PSTN Trunk tab's "Commit + # The last line (docker restart) backs the Extensions tab's "Commit # Changes" button — see restart_asterisk_container()'s comment for why # that exists (AST_CONFIG() live-reads not always picking up dashboard # edits without a full container restart). @@ -1241,8 +1250,8 @@ def write_permission(ext, tier, numbers_raw, messaging_enabled=False): def write_messaging(ext, enabled): """Sets/clears just the messaging flag for one extension, leaving any tier/allowed_numbers/personal_did untouched. This is the write path for - the standalone "Internal SIP messaging" card, which works whether or - not a PSTN trunk has ever been installed — messaging has no dependency + the Extensions tab's Messaging column when there's no PSTN trunk to + save alongside — messaging works whether or not one has been installed — messaging has no dependency on one (no cost, no carrier, no DID), unlike the calling-permissions table this dashboard otherwise gates behind pstn_installed(). Creates pstn-permissions.conf from scratch if it doesn't exist yet.""" @@ -1373,9 +1382,9 @@ def pstn_installed(): """True only once services/pstn-trunk.sh has actually wired the dialplan in (pstn-trunk-dialplan.conf existing), not just because base Asterisk is present — pjsip.conf/extensions.conf exist either way, so extension names - alone can't tell us this. Without this check the tab would show a real - extension list and a default-but-unenforced 10/10 cap even when there is - no PSTN trunk at all.""" + alone can't tell us this. Without this check the Extensions tab would + show tier columns and a default-but-unenforced 10/10 cap even when there + is no PSTN trunk at all.""" if not ASTERISK_CONFIG_DIR: return False return os.path.isfile(os.path.join(ASTERISK_CONFIG_DIR, "pstn-trunk-dialplan.conf")) @@ -1583,7 +1592,7 @@ def remove_personal_did(did): return True, "Removed %s" % did -# ── Easy Asterisk Admin (native — devices, categories, rooms/ring-groups) ── +# ── Easy Asterisk device management (devices, categories, rooms/ring-groups) ── # Full reimplementation of vendor/easy-asterisk/easy-asterisk-v0.10.0.sh's # vendored web admin (its own separate process, normally reached via its own # port/domain) as native code here instead — one tab, one process, no @@ -1651,7 +1660,7 @@ def ea_rebuild_dialplan(): def restart_asterisk_container(): """Restarts the Easy Asterisk container - the "Commit Changes" button on - the PSTN Trunk tab. Confirmed live: dashboard writes to + the Extensions tab. Confirmed live: dashboard writes to pstn-permissions.conf/pstn-groups.conf/pstn-personal-dids.conf land on disk immediately (readable via a plain `cat` right after saving), but AST_CONFIG() in the dialplan sometimes kept returning a stale value @@ -1662,7 +1671,7 @@ def restart_asterisk_container(): throughout), but the restart reliably clears it, so this button exists instead of requiring every admin to rediscover "just restart it" the hard way. Uses the same ASTERISK_EA_CONTAINER/run_sudo mechanism as the - Easy Asterisk Admin tab's own docker exec calls - no new sudoers scope + Extensions tab's own docker exec calls - no new sudoers scope needed beyond the one line added for this.""" if not ASTERISK_EA_CONTAINER: return False, "No Asterisk container detected on this box" @@ -2258,16 +2267,23 @@ INDEX_HTML = """ .muted { color: #9aa4b2; font-size: 0.85rem; } a { color: #4f8cff; } #msg { margin-top: 0.5rem; font-size: 0.85rem; } + /* Capability gating for the Extensions tab. Everything that needs the Easy + Asterisk container (device/category/room writes) is .ea-only; everything + that needs a PSTN trunk dialplan is .pstn-only. Both classes start ON the + body so nothing flashes before /api/ea-status and /api/pstn-status answer, + and they're removed once those confirm. Marking cells rather than juggling + column indices keeps the one extensions table honest as columns come and + go. */ + body.no-ea .ea-only { display: none !important; } + body.no-pstn .pstn-only { display: none !important; } - +

Security Dashboard

@@ -2309,35 +2325,7 @@ INDEX_HTML = """ -