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:
Claude
2026-07-23 14:15:17 +00:00
parent 3359898b4e
commit b10a626b22
4 changed files with 375 additions and 15 deletions
+192
View File
@@ -293,6 +293,190 @@ $_ea_dir/logs/full {
LOGROTATE
}
# ── Shared: extension presence (online/offline) ntfy alerts ────────────────
# Polls PJSIP registration state and alerts only on a CHANGE from the last
# check (never on every poll) — same periodic-check shape as pstn-trunk.sh's
# usage-alert script, but purely informational, so a looser 2-minute
# interval is fine here (nothing enforces/blocks anything off the back of
# this one). UNVERIFIED: the `pjsip show contacts` column layout below is
# parsed defensively (grep for the Avail/Unavail keyword rather than a fixed
# column position) specifically because it hasn't been confirmed against a
# live install's actual output yet — run
# `docker exec easy-asterisk-do asterisk -rx "pjsip show contacts"` yourself
# after enabling this to confirm extensions/status actually show up as
# expected, same as any other not-yet-live-tested piece in this project.
_asterisk_do_write_presence_alert_script() {
local FILE="$1" CONTAINER_NAME="$2" NTFY_URL="$3" STATE_FILE="$4"
cat > "$FILE" << 'SCRIPT'
#!/bin/bash
# Auto-generated by services/asterisk-digital-ocean.sh — rerun the installer's
# presence-alert step to change settings instead of editing this directly.
CONTAINER_NAME="__PRESENCE_CONTAINER__"
NTFY_URL="__PRESENCE_NTFY_URL__"
STATE_FILE="__PRESENCE_STATE_FILE__"
[[ -z "$NTFY_URL" ]] && exit 0
send_ntfy() {
curl -m 5 -s -d "$1" "$NTFY_URL" >/dev/null 2>&1
}
CURRENT="$(docker exec "$CONTAINER_NAME" asterisk -rx "pjsip show contacts" 2>/dev/null | grep '^ Contact:' | while read -r _ aor rest; do
ext="${aor%%/*}"
status="Unknown"
case "$rest" in
*Unavail*) status="Unavail" ;;
*Avail*) status="Avail" ;;
esac
echo "${ext}:${status}"
done)"
[[ -z "$CURRENT" ]] && exit 0
touch "$STATE_FILE"
declare -A OLD_STATE
while IFS=: read -r ext status; do
[[ -n "$ext" ]] && OLD_STATE["$ext"]="$status"
done < "$STATE_FILE"
: > "${STATE_FILE}.new"
while IFS=: read -r ext status; do
[[ -z "$ext" ]] && continue
echo "${ext}:${status}" >> "${STATE_FILE}.new"
old="${OLD_STATE[$ext]:-}"
if [[ -n "$old" && "$old" != "$status" && "$status" != "Unknown" ]]; then
if [[ "$status" == "Avail" ]]; then
send_ntfy "Extension $ext is back online."
elif [[ "$old" == "Avail" ]]; then
send_ntfy "Extension $ext went offline."
fi
fi
done <<< "$CURRENT"
mv "${STATE_FILE}.new" "$STATE_FILE"
SCRIPT
sed -i "s#__PRESENCE_CONTAINER__#${CONTAINER_NAME}#g; s#__PRESENCE_NTFY_URL__#${NTFY_URL}#g; s#__PRESENCE_STATE_FILE__#${STATE_FILE}#g" "$FILE"
chmod 755 "$FILE"
}
_asterisk_do_install_presence_timer() {
local EA_DIR="$1"
mkdir -p "$EA_DIR/logs"
if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
cat > /etc/systemd/system/asterisk-presence-alert.service << SVCEOF
[Unit]
Description=Asterisk extension presence (online/offline) check
[Service]
Type=oneshot
ExecStart=/bin/bash $EA_DIR/asterisk-presence-alert.sh
StandardOutput=append:$EA_DIR/logs/asterisk-presence-alert.log
StandardError=append:$EA_DIR/logs/asterisk-presence-alert.log
SVCEOF
cat > /etc/systemd/system/asterisk-presence-alert.timer << SVCEOF
[Unit]
Description=Run the Asterisk presence check every 2 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=2min
AccuracySec=10s
[Install]
WantedBy=timers.target
SVCEOF
systemctl daemon-reload
systemctl enable --now asterisk-presence-alert.timer
log_success "Presence check installed (systemd timer, every 2 minutes)."
elif command -v cron >/dev/null 2>&1 || [[ -d /etc/cron.d ]]; then
cat > /etc/cron.d/asterisk-presence-alert << CRON
*/2 * * * * root /bin/bash $EA_DIR/asterisk-presence-alert.sh >> $EA_DIR/logs/asterisk-presence-alert.log 2>&1
CRON
log_success "Presence check installed (cron.d fallback — systemd not detected)."
else
log_warning "Neither systemd nor cron available — run $EA_DIR/asterisk-presence-alert.sh manually/periodically."
fi
}
_asterisk_do_remove_presence_timer() {
systemctl disable --now asterisk-presence-alert.timer 2>/dev/null || true
rm -f /etc/systemd/system/asterisk-presence-alert.timer /etc/systemd/system/asterisk-presence-alert.service
rm -f /etc/cron.d/asterisk-presence-alert
systemctl daemon-reload 2>/dev/null || true
}
# Interactive step — called from both the fresh-install flow and "update in
# place" (always asked either way, same reasoning as pstn-trunk.sh's
# international-calling step: this is a live-editable extra, not a
# structural setting, so it doesn't belong exclusively to one path).
_asterisk_do_run_presence_step() {
local EA_DIR="$1"
local SETTINGS_FILE="$EA_DIR/.presence-alert.env"
local STATE_FILE="$EA_DIR/.presence-alert.state"
echo ""
local _CUR_ENABLED="n" _CUR_NTFY=""
if [[ -f "$SETTINGS_FILE" ]]; then
# shellcheck disable=SC1090
source "$SETTINGS_FILE"
_CUR_ENABLED="${PRESENCE_ENABLED:-n}"
_CUR_NTFY="${PRESENCE_NTFY_URL:-}"
fi
if [[ "$_CUR_ENABLED" == "y" ]]; then
echo " Extension online/offline ntfy alerts are ON (topic: $_CUR_NTFY)."
local _CHANGE=""
prompt_yn " Change or disable this? (y/n):" "n" _CHANGE
[[ "$_CHANGE" =~ ^[Yy]$ ]] || return 0
local _DISABLE=""
prompt_yn " Disable presence alerts entirely? (y/n):" "n" _DISABLE
if [[ "$_DISABLE" =~ ^[Yy]$ ]]; then
_asterisk_do_remove_presence_timer
rm -f "$EA_DIR/asterisk-presence-alert.sh" "$STATE_FILE"
cat > "$SETTINGS_FILE" << ENV
PRESENCE_ENABLED="n"
PRESENCE_NTFY_URL=""
ENV
log_success "Presence alerts disabled."
return 0
fi
else
local _WANT=""
prompt_yn "Send an ntfy alert when an extension's SIP registration goes offline / comes back online? (y/n):" "n" _WANT
[[ "$_WANT" =~ ^[Yy]$ ]] || return 0
fi
local _ntfy_default="${_CUR_NTFY:-https://ntfy.sh/asterisk-presence}"
if [[ -z "$_CUR_NTFY" ]] && [[ -f "$DOCKER_DIR/ntfy/config/server.yml" ]]; then
local _local_base_url
_local_base_url="$(grep -oP '(?<=base-url: ")[^"]+' "$DOCKER_DIR/ntfy/config/server.yml" 2>/dev/null || true)"
if [[ -n "$_local_base_url" ]] && [[ "$_local_base_url" != "https://ntfy.example.com" ]]; then
_ntfy_default="${_local_base_url}/asterisk-presence"
log_info "Detected a configured local ntfy instance at $_local_base_url — using it as the default."
fi
fi
local PRESENCE_NTFY_URL=""
prompt_text " ntfy topic URL:" "$_ntfy_default" PRESENCE_NTFY_URL
if [[ -z "$PRESENCE_NTFY_URL" ]]; then
log_warning "No topic entered — presence alerts not enabled."
return 0
fi
_asterisk_do_write_presence_alert_script "$EA_DIR/asterisk-presence-alert.sh" "easy-asterisk-do" "$PRESENCE_NTFY_URL" "$STATE_FILE"
_asterisk_do_install_presence_timer "$EA_DIR"
cat > "$SETTINGS_FILE" << ENV
PRESENCE_ENABLED="y"
PRESENCE_NTFY_URL="${PRESENCE_NTFY_URL}"
ENV
chown "$ACTUAL_USER:$ACTUAL_USER" "$SETTINGS_FILE" 2>/dev/null || true
log_success "Presence alerts enabled (checked every 2 minutes) — topic: $PRESENCE_NTFY_URL"
log_info "Fires only on a state CHANGE, never every check — the first check after enabling"
log_info "never alerts by itself, since there's no prior state to compare against yet."
}
# ── Shared: docker-compose.yml ─────────────────────────────────────────────
# Same reasoning as above — one copy of the template used by both fresh
# installs and updates. Must be called with $PWD already at $EA_DIR.
@@ -380,6 +564,9 @@ install_asterisk-digital-ocean() {
echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP if Caddy is already installed (needed for cert sync)"
echo "[DRY-RUN] Would offer local OR remote Authelia to protect the web admin, if either is already available"
echo "[DRY-RUN] Would offer 'update in place' instead of a fresh install if $EA_DIR already exists"
echo "[DRY-RUN] Would offer optional ntfy alerts on extension registration going offline/online"
echo "[DRY-RUN] (checked every 2 minutes via systemd timer, cron.d fallback; always asked,"
echo "[DRY-RUN] update mode included)"
return 0
fi
@@ -411,6 +598,8 @@ install_asterisk-digital-ocean() {
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi
_asterisk_do_run_presence_step "$EA_DIR"
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
@@ -839,6 +1028,9 @@ CADDY_BLOCK
log_info "It auto-detects this asterisk-digital-ocean install and wires up SIP protection on its own."
fi
# ── Extension presence (online/offline) ntfy alerts ────────────────────────
_asterisk_do_run_presence_step "$EA_DIR"
# ── README ────────────────────────────────────────────────────────────────
write_readme "$EA_DIR" << MD
# Easy Asterisk PBX + coturn — DigitalOcean droplet edition
+56 -5
View File
@@ -110,6 +110,41 @@ install_crowdsec() {
return 0
fi
# ── Existing install? Offer update-in-place instead of a full reconfigure ─
# Everything below (ASN exemptions, geo-allowlist, ntfy, remote LAPI) is
# additive/idempotent at the file level, but there was no gate at all
# here before — every rerun re-asked all four optional questions
# unconditionally, which reads as "reconfigure" rather than "update" even
# though nothing already-configured was actually being destroyed.
local _REMOTE_LAPI_PENDING=""
local ASK_OPTIONAL=true
if command -v cscli &> /dev/null; then
echo " CrowdSec is already installed."
local CS_MODE=""
prompt_reinstall_mode CS_MODE 2>/dev/null || {
# prompt_reinstall_mode isn't defined in the standalone stub — fall
# back to a plain yes/no when run outside the full repo.
local _r=""
prompt_yn " Reconfigure the optional settings below (ASN exemptions, geo-allowlist, ntfy, remote LAPI)? (y/n):" "n" _r
[ "$_r" = "y" ] || [ "$_r" = "Y" ] && CS_MODE="fresh" || CS_MODE="update"
}
case "$CS_MODE" in
update)
ASK_OPTIONAL=false
log_info "Refreshing agent/collections/acquisitions only — ASN exemptions,"
log_info "geo-allowlist, ntfy alerts, and remote-LAPI settings are left exactly as"
log_info "they are. Choose 'fresh' instead to revisit any of those."
;;
cancel)
log_info "Leaving the existing CrowdSec install as-is."
return 0
;;
fresh)
log_info "Proceeding with a full reconfigure — every optional prompt below runs again."
;;
esac
fi
# ── 1. Install the CrowdSec agent ────────────────────────────────────────
if command -v cscli &> /dev/null; then
echo " ✓ CrowdSec is already installed"
@@ -198,7 +233,9 @@ labels:
# the hub originals so there's no double-processing of the same events.
echo ""
local ASN_EXEMPT=""
prompt_yn "Exempt specific carrier ASNs from Asterisk brute-force bans only? (y/n):" "n" ASN_EXEMPT
if [ "$ASK_OPTIONAL" = true ]; then
prompt_yn "Exempt specific carrier ASNs from Asterisk brute-force bans only? (y/n):" "n" ASN_EXEMPT
fi
if [ "$ASN_EXEMPT" = "y" ] || [ "$ASN_EXEMPT" = "Y" ]; then
sudo cscli collections install crowdsecurity/geoip-enrich 2>/dev/null || true
echo " ASNs observed live: T-Mobile 21928, Starlink 14593. A carrier can operate"
@@ -295,7 +332,9 @@ ASTENUM
echo " https://app.crowdsec.net/"
echo ""
local GEO_ALLOWLIST=""
prompt_yn "Restrict Caddy-fronted web traffic to North America + Europe only (block every other country)? Does NOT affect SSH. (y/n):" "n" GEO_ALLOWLIST
if [ "$ASK_OPTIONAL" = true ]; then
prompt_yn "Restrict Caddy-fronted web traffic to North America + Europe only (block every other country)? Does NOT affect SSH. (y/n):" "n" GEO_ALLOWLIST
fi
if [ "$GEO_ALLOWLIST" = "y" ] || [ "$GEO_ALLOWLIST" = "Y" ]; then
echo " Installing geoip-enrich (tags every event with a country code; no MaxMind"
echo " account needed — CrowdSec bundles its own redistributable GeoLite2 data)..."
@@ -353,7 +392,9 @@ labels:
# ── 7. Optional: push ban alerts to ntfy ─────────────────────────────────
echo ""
local CS_NTFY=""
prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY
if [ "$ASK_OPTIONAL" = true ]; then
prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY
fi
if [ "$CS_NTFY" = "y" ] || [ "$CS_NTFY" = "Y" ]; then
# Prefer a locally-installed ntfy's own base-url as the default, if one
# exists and actually looks configured (not still the placeholder
@@ -420,8 +461,10 @@ headers:
# of every box running its own. Useful if you already have CrowdSec on
# a homelab and don't want a second LAPI+SQLite DB on this droplet.
echo ""
local USE_REMOTE_LAPI="" _REMOTE_LAPI_PENDING=""
prompt_yn "Point this agent at a remote/central LAPI instead of running its own (e.g. one already on a homelab)? (y/n):" "n" USE_REMOTE_LAPI
local USE_REMOTE_LAPI=""
if [ "$ASK_OPTIONAL" = true ]; then
prompt_yn "Point this agent at a remote/central LAPI instead of running its own (e.g. one already on a homelab)? (y/n):" "n" USE_REMOTE_LAPI
fi
if [ "$USE_REMOTE_LAPI" = "y" ] || [ "$USE_REMOTE_LAPI" = "Y" ]; then
echo ""
echo " This registers this machine and disables its local API server."
@@ -506,6 +549,14 @@ install. The real configuration lives under `/etc/crowdsec`.
before they ever touch your services.
- Optionally enriches events with **geo/ASN** data for geo-blocking.
## Rerunning this script
Rerunning offers **update** (refreshes the agent/collections/acquisitions
only — ASN exemptions, geo-allowlist, ntfy, and remote-LAPI settings are left
exactly as they are) or **fresh** (revisit every optional prompt again, same
as a first install). Nothing here is destructive either way — "fresh" only
overwrites a setting if you actually answer its prompt differently.
## Key commands
```
+28 -1
View File
@@ -260,12 +260,26 @@ base-url: "${NTFY_BASE_URL:-https://ntfy.example.com}" # UPDATE to your actual
cache-file: /var/cache/ntfy/cache.db
cache-duration: 12h
auth-file: /var/cache/ntfy/auth.db
auth-default-access: deny-all
# read-write, not deny-all: with an auth-file present, ntfy auth-checks every
# request, and deny-all blocks anonymous publish/subscribe on EVERY topic
# unless you separately create a user and grant a per-topic ACL (nothing in
# this installer, crowdsec.sh, or pstn-trunk.sh does that) — every ntfy alert
# they curl to a topic silently 403s, and a plain phone subscription gets
# nothing, with no visible error either side. read-write matches public
# ntfy.sh's own model instead: anyone who knows/guesses the topic name can
# read/publish it, so treat topic names as a shared secret (long/random for
# anything sensitive) rather than relying on auth you'd have to wire up
# per-script. Confirmed live: this was the actual cause of a self-hosted
# instance never delivering alerts despite a correct topic subscription.
auth-default-access: read-write
behind-proxy: true
NTFY_CFG
[[ -n "$NTFY_BASE_URL" ]] \
&& log_info "base-url set to $NTFY_BASE_URL — update if domain changes" \
|| log_warning "base-url set to placeholder — edit config/server.yml after install"
log_info "auth-default-access: read-write — topics are open to anyone who knows the"
log_info "topic name (same model as public ntfy.sh). Use long/random topic names for"
log_info "anything sensitive, or lock down individual topics later with 'ntfy access'."
fi
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR"
@@ -288,6 +302,19 @@ phone or browser.
- Send a notification: \`curl -d "Hello!" localhost:8090/mytopic\`
- Subscribe on phone: ntfy app -> Add subscription -> localhost:8090/mytopic
## Access model
\`auth-default-access: read-write\` — anyone who knows a topic name can read
and publish to it, same as public ntfy.sh. There's no per-script login wired
up (crowdsec.sh, pstn-trunk.sh, etc. all just \`curl -d ... \$topic_url\` with
no auth), so this is what makes those alerts actually arrive. Treat topic
names as a shared secret — use long/random ones for anything sensitive — or
lock a specific topic down individually:
\`\`\`
docker exec ntfy ntfy user add myuser
docker exec ntfy ntfy access myuser mytopic read-write
docker exec ntfy ntfy access everyone mytopic deny
\`\`\`
## Data
- Config: ./config (mounted to /etc/ntfy)
- Cache: ./cache (mounted to /var/cache/ntfy)
+99 -9
View File
@@ -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 &#8599;</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 &#8599;</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 => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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();