diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh
index 564e8e7..fc8dcf5 100644
--- a/services/security-dashboard.sh
+++ b/services/security-dashboard.sh
@@ -194,52 +194,24 @@ 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.
+
- **Security Log** — parses \`$ASTERISK_LOG_DIR/full\` for SIP auth failures
(wrong password, unknown extension, etc.) with timestamp/account/remote IP,
- filterable per column (each header has its own text filter, live as you type).
-- **CrowdSec** — current bans (\`cscli decisions list\`), a delete/unban button
- per entry, carrier/ASN + country columns, and management of the ASN-exempt
- Asterisk brute-force scenarios (see \`services/crowdsec.sh\`'s "Exempt
- specific carrier ASNs" option) without SSHing in:
- - **Currently-exempt ASNs** are listed with carrier name (resolved from
- current bans, falling back to alert history for ASNs with no active ban
- right now) regardless of when they were added.
- - **Unwhitelist** removes an ASN from the exemption list — future Asterisk
- auth failures from it are evaluated normally again.
- - **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP
- CrowdSec has ever recorded for that ASN, for accidental-whitelist cases
- where you don't want to wait for it to misbehave again.
-- **PSTN Trunk** — a **Groups** card at the top (always available, whether
- or not a PSTN trunk has ever been installed) 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. Below that, the rest of the tab detects
- whether \`services/pstn-trunk.sh\`'s dialplan is
- actually installed (\`pstn-trunk-dialplan.conf\` present) and shows a
- clear "not installed" message instead of the calling-permissions editor
- if not, so it never shows real-looking-but-unenforced defaults. When
- installed: 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. Also manages personal-number assignments (DID ->
- owner extension), 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. An "Internal SIP
- messaging" card at the **bottom** of the tab (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) is always available
- regardless of any of the above.
+ sortable per column (click a header to sort, click again to reverse).
- **Asterisk Admin** — an embedded, lazy-loaded iframe of the real Asterisk
web admin (only fetched the first time you open the tab), plus an
- "open in a new tab" fallback link that's always there regardless. Only
- shows up once an Asterisk install is detected. If a local Caddy install is
- found for both this dashboard and the Asterisk admin's own domain, install
- automatically patches the admin's Caddy site block from
+ "open in a new tab" fallback link that's always there regardless. Its nav
+ button only appears once an Asterisk install is detected. If a local Caddy
+ install is found for both this dashboard and the Asterisk admin's own
+ domain, install automatically patches the admin's Caddy site block from
`X-Frame-Options` to a `Content-Security-Policy: frame-ancestors` entry
naming only this dashboard's domain, so the browser actually allows the
frame — every other site is still refused framing exactly as before. This
@@ -248,6 +220,43 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly.
confirmed against Authelia's own portal-framing behavior on a live
install) — if the tab shows a blank frame, use the fallback link and check
this service's own log output from install time for a manual one-line fix.
+- **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.
+- **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
+ management of the ASN-exempt Asterisk brute-force scenarios (see
+ \`services/crowdsec.sh\`'s "Exempt specific carrier ASNs" option) without
+ SSHing in:
+ - **Currently-exempt ASNs** are listed with carrier name (resolved from
+ current bans, falling back to alert history for ASNs with no active ban
+ right now) regardless of when they were added.
+ - **Unwhitelist** removes an ASN from the exemption list — future Asterisk
+ auth failures from it are evaluated normally again.
+ - **Unwhitelist + Ban** does that *and* immediately bans (24h) every IP
+ CrowdSec has ever recorded for that ASN, for accidental-whitelist cases
+ where you don't want to wait for it to misbehave again.
## Manage
\`\`\`
@@ -814,6 +823,14 @@ def run_sudo(args, timeout=15):
return False, "", str(e)
+def crowdsec_installed():
+ """True if cscli is actually present on this host — mirrors
+ pstn_installed()'s approach of checking for the real thing rather than a
+ stored flag, so the CrowdSec tab tracks live state without needing this
+ dashboard reinstalled after CrowdSec is added or removed."""
+ return os.path.isfile("/usr/bin/cscli")
+
+
def get_decisions():
ok, out, err = run_sudo(["/usr/bin/cscli", "decisions", "list", "-o", "json"])
if not ok or not out.strip():
@@ -1524,9 +1541,10 @@ INDEX_HTML = """
Security Dashboard
@@ -1566,7 +1584,7 @@ INDEX_HTML = """
-
+
Groups
@@ -1584,11 +1602,17 @@ INDEX_HTML = """
-
-
PSTN trunk not installed
-
No PSTN trunk dialplan was found on this box — sudo ./setup.sh pstn-trunk hasn't been run (or its config was removed). The calling permissions/caps/personal-numbers below aren't enforced yet; install it first to use them. Internal SIP messaging (bottom of this tab) works independently of this.
+
+
Internal SIP messaging
+
+ Asterisk's native SIP texting between extensions — no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk being installed at all. Enforced live by a dedicated dialplan context (see services/asterisk-digital-ocean.sh's README) — install/rerun that service to pick up the dialplan wiring if this box predates it.
+
+
+
+
-
+
+
Concurrent-call caps
A call over either cap gets a busy signal (and an ntfy alert, if enabled) — existing calls are never affected. Changes apply live, on the next call.
@@ -1608,7 +1632,7 @@ INDEX_HTML = """
Changes apply live, on the next call — no Asterisk restart needed.
- Messaging — Asterisk's native internal SIP texting (no carrier SMS, no PSTN, no cost), independent of the calling tier. Enforced live by a dedicated dialplan context — see services/asterisk-digital-ocean.sh's README for how, and its caveat on the sender-extraction logic still needing real-traffic confirmation.
+ Messaging — the same internal SIP texting flag as the Extensions tab's checkboxes, independent of the calling tier; this column is just a convenience for setting it alongside tier/numbers on one row. Enforced live by a dedicated dialplan context — see services/asterisk-digital-ocean.sh's README for how, and its caveat on the sender-extraction logic still needing real-traffic confirmation.
Ext
@@ -1637,16 +1661,6 @@ INDEX_HTML = """
-
-
-
Internal SIP messaging
-
- Asterisk's native SIP texting between extensions — no carrier SMS, no PSTN, no cost, and no dependency on a PSTN trunk being installed at all. Independent of the calling permissions above. Enforced live by a dedicated dialplan context (see services/asterisk-digital-ocean.sh's README) — install/rerun that service to pick up the dialplan wiring if this box predates it.
-