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.
This commit is contained in:
@@ -141,7 +141,7 @@ install_security-dashboard() {
|
||||
prompt_yn "Reconfigure this dashboard's Caddy protection (Authelia domain, or add/rotate an independent Basic Auth layer)? (y/n):" "n" _reconf
|
||||
if [[ "$_reconf" =~ ^[Yy]$ ]]; then
|
||||
_secdash_remove_caddy_block "$DASHBOARD_PORT"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
@@ -184,7 +184,7 @@ install_security-dashboard() {
|
||||
# _secdash_configure_caddy so "update" mode can also offer to reconfigure
|
||||
# it later (e.g. to add Basic Auth to an already-deployed dashboard)
|
||||
# without duplicating this logic — see that function for the rest.
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT"
|
||||
_secdash_configure_caddy "$DASHBOARD_PORT" "$ASTERISK_ADMIN_URL"
|
||||
|
||||
write_readme "$APP_DIR" << README_MD
|
||||
# Security Dashboard
|
||||
@@ -236,7 +236,20 @@ not in Docker — it needs to call \`cscli\` and read Asterisk's log directly.
|
||||
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.
|
||||
- Link to the Asterisk web admin itself (doesn't embed it, just links out).
|
||||
- **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
|
||||
`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
|
||||
is best-effort (it depends on matching the exact header line
|
||||
`services/asterisk-digital-ocean.sh` itself writes, and hasn't been
|
||||
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.
|
||||
|
||||
## Manage
|
||||
\`\`\`
|
||||
@@ -378,7 +391,7 @@ SUDOERS
|
||||
# retroactively) using the exact same code path as a fresh install, instead
|
||||
# of hand-patching a live Caddyfile block in place.
|
||||
_secdash_configure_caddy() {
|
||||
local DASHBOARD_PORT="$1"
|
||||
local DASHBOARD_PORT="$1" ADMIN_URL="${2:-}"
|
||||
|
||||
echo ""
|
||||
if ! command -v docker &>/dev/null || ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$"; then
|
||||
@@ -509,6 +522,66 @@ CADDYBLOCK
|
||||
ufw_allow_from_caddy_net "${DASHBOARD_PORT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
_secdash_allow_asterisk_admin_iframe "$ADMIN_URL" "$SD_DOMAIN"
|
||||
}
|
||||
|
||||
# Best-effort: lets the dashboard's "Asterisk Admin" tab iframe-embed the
|
||||
# real Asterisk web admin, by swapping that domain's own Caddy site block
|
||||
# from X-Frame-Options to a CSP frame-ancestors entry naming ONLY this
|
||||
# dashboard's domain — every other site is still refused framing exactly as
|
||||
# before, this just relaxes it for the one origin that's supposed to embed
|
||||
# it. Best-effort because it depends on finding the exact
|
||||
# X-Frame-Options line services/asterisk-digital-ocean.sh itself generates,
|
||||
# inside a live Caddyfile it doesn't own — if that block was hand-edited
|
||||
# since, or doesn't exist yet (Asterisk installed after this dashboard, or
|
||||
# no local Caddy at all), this silently does nothing and the tab's "open in
|
||||
# a new tab" fallback link still works either way.
|
||||
_secdash_allow_asterisk_admin_iframe() {
|
||||
local ADMIN_URL="$1" SD_DOMAIN="$2"
|
||||
[ -n "$ADMIN_URL" ] || return 0
|
||||
[ -n "$SD_DOMAIN" ] || return 0
|
||||
command -v docker &>/dev/null || return 0
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^caddy$" || return 0
|
||||
|
||||
local ADMIN_DOMAIN="${ADMIN_URL#https://}"
|
||||
ADMIN_DOMAIN="${ADMIN_DOMAIN#http://}"
|
||||
local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile"
|
||||
[ -f "$CADDY_FILE" ] || return 0
|
||||
grep -q "^${ADMIN_DOMAIN} {" "$CADDY_FILE" || return 0
|
||||
|
||||
if grep -qF "frame-ancestors 'self' https://${SD_DOMAIN};" "$CADDY_FILE"; then
|
||||
return 0 # already patched for this exact dashboard domain
|
||||
fi
|
||||
|
||||
local CSP_LINE=" Content-Security-Policy \"frame-ancestors 'self' https://${SD_DOMAIN};\""
|
||||
local TMP_FILE
|
||||
TMP_FILE="$(mktemp)"
|
||||
awk -v domain="${ADMIN_DOMAIN} {" -v csp="$CSP_LINE" '
|
||||
BEGIN { in_block = 0; patched = 0 }
|
||||
index($0, domain) == 1 { in_block = 1 }
|
||||
in_block && !patched && /X-Frame-Options/ { print csp; patched = 1; next }
|
||||
{ print }
|
||||
in_block && /^}/ { in_block = 0 }
|
||||
' "$CADDY_FILE" > "$TMP_FILE"
|
||||
|
||||
if grep -qF "frame-ancestors 'self' https://${SD_DOMAIN};" "$TMP_FILE"; then
|
||||
cp "$CADDY_FILE" "$CADDY_FILE.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
mv "$TMP_FILE" "$CADDY_FILE"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || docker restart caddy &>/dev/null; then
|
||||
log_success "Asterisk web admin (${ADMIN_DOMAIN}) now allows embedding from https://${SD_DOMAIN} — the dashboard's Asterisk Admin tab should load it."
|
||||
else
|
||||
log_warning "Caddyfile patched, but reload/restart failed — check: docker logs caddy"
|
||||
fi
|
||||
else
|
||||
rm -f "$TMP_FILE"
|
||||
log_warning "Couldn't find an X-Frame-Options line in ${ADMIN_DOMAIN}'s Caddy block to patch —"
|
||||
log_warning "the dashboard's Asterisk Admin tab will show a blank frame. Add this line yourself"
|
||||
log_warning "inside that domain's 'header { }' block in $CADDY_FILE, replacing X-Frame-Options:"
|
||||
log_warning " Content-Security-Policy \"frame-ancestors 'self' https://${SD_DOMAIN};\""
|
||||
log_warning "then: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
}
|
||||
|
||||
# Removes the dashboard's existing Caddyfile site block (found via its
|
||||
@@ -1433,8 +1506,8 @@ INDEX_HTML = """<!doctype html>
|
||||
<button class="tab-btn active" data-tab="security">Security Log</button>
|
||||
<button class="tab-btn" data-tab="crowdsec">CrowdSec</button>
|
||||
<button class="tab-btn" data-tab="pstn">PSTN Trunk</button>
|
||||
<button class="tab-btn" id="asterisk-tab-btn" data-tab="asterisk" style="display:none">Asterisk Admin</button>
|
||||
</nav>
|
||||
<a id="admin-link" href="#" target="_blank" style="display:none">Asterisk Web Admin ↗</a>
|
||||
</header>
|
||||
<main>
|
||||
<div id="tab-security">
|
||||
@@ -1542,17 +1615,35 @@ INDEX_HTML = """<!doctype html>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-asterisk" style="display:none">
|
||||
<div class="card">
|
||||
<p class="muted">
|
||||
Embedded — not a copy, this is the real Asterisk web admin loaded live in a frame.
|
||||
If it logs you in separately (its own Authelia domain, or Basic Auth), that's expected —
|
||||
it's still a genuinely separate site under the hood.
|
||||
<a id="admin-link-fallback" href="#" target="_blank">Open in a new tab instead ↗</a>
|
||||
</p>
|
||||
<iframe id="admin-iframe" style="width:100%;height:80vh;border:1px solid #2a2e38;border-radius:8px;background:#0f1115"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
function esc(s) { return (s || "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
||||
|
||||
const TABS = ["security", "crowdsec", "pstn"];
|
||||
const TABS = ["security", "crowdsec", "pstn", "asterisk"];
|
||||
document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
TABS.forEach(t => { document.getElementById("tab-" + t).style.display = btn.dataset.tab === t ? "" : "none"; });
|
||||
if (btn.dataset.tab === "pstn") { loadPstnStatus(); loadMessaging(); loadGroups(); }
|
||||
if (btn.dataset.tab === "asterisk") {
|
||||
// Lazy-loaded — only fetched the first time this tab is opened, not
|
||||
// on every dashboard page load (avoids an extra login prompt/request
|
||||
// to a separate site for people who never open this tab).
|
||||
const frame = document.getElementById("admin-iframe");
|
||||
if (!frame.src && adminUrl) frame.src = adminUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1933,9 +2024,8 @@ async function removePersonalDid(did) {
|
||||
|
||||
const adminUrl = "__ASTERISK_ADMIN_URL__";
|
||||
if (adminUrl) {
|
||||
const link = document.getElementById("admin-link");
|
||||
link.href = adminUrl;
|
||||
link.style.display = "";
|
||||
document.getElementById("asterisk-tab-btn").style.display = "";
|
||||
document.getElementById("admin-link-fallback").href = adminUrl;
|
||||
}
|
||||
|
||||
loadSecurity();
|
||||
|
||||
Reference in New Issue
Block a user