From 2b926ebf0bdf91ea22ace43ca3c2cc3b9de5d13a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 20:32:33 +0000 Subject: [PATCH 1/3] security-dashboard: widen main container for the Extensions table The previous "box too narrow" fix targeted the QR popup, but the actual complaint (confirmed by screenshot) was the Extensions table itself -- ten columns (Ext/Name/Mobile/Status/Transport/PSTN/Whitelist/Messaging/ Voicemail/actions) forced .table-wrap's horizontal scrollbar even on a normal desktop viewport because main was capped at 1180px. Bumped to 1600px; verified via headless render at 1280-1920px that the table no longer overflows. --- services/security-dashboard.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 385de4e..766cf5f 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -3632,7 +3632,14 @@ INDEX_HTML = """ } nav button:hover { color: var(--text); } nav button.active { color: var(--text); border-bottom-color: var(--accent); } - main { padding: var(--sp-6); max-width: 1180px; margin: 0 auto; } + /* 1180 was too narrow for the Extensions table specifically (Ext, Name, + Mobile, Status, Transport, PSTN, Whitelist, Messaging, Voicemail, plus + the row-action column) -- ten columns including a dropdown and a free-text + whitelist field forced .table-wrap's horizontal scrollbar even on a normal + desktop viewport. 1600 gives every tab's tables room without it; narrow + viewports still fall back to that same scrollbar (.table-wrap already + handles it), this only raises the ceiling for wide ones. */ + main { padding: var(--sp-6); max-width: 1600px; margin: 0 auto; } /* ── Cards ────────────────────────────────────────────────────────────── */ .card { From 893ab19759a790627baa16e387d72756970fa462 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:09:25 +0000 Subject: [PATCH 2/3] asterisk: warn non-DO public installs about provider-side firewalls FQDN mode was already available outside DigitalOcean detection (the home/LAN path's "Networking mode" menu offers it), but only DO installs got any reminder about a network-edge firewall sitting in front of the box -- non-DO public VPS installs got no equivalent, and UFW being wide open gives no signal that a separate provider-managed firewall exists at all. Confirmed live on an IONOS VPS: UFW allowed every SIP/TURN/RTP port, Asterisk's own PJSIP logger showed zero incoming packets, and nothing in the installer's own output pointed at the cause -- IONOS's own network firewall (Cloud Panel -> Networking -> Firewall Policies) only allowed 22/80/443/8443/8447 and silently dropped the rest before it ever reached the box. Adds _asterisk_remind_non_do_firewall(), fired whenever a fresh install sets a public FQDN without being in DO/droplet mode: same port list as what UFW just opened, plus a pointer at the IONOS console location as a concrete example other providers can generalize from. --- services/asterisk.sh | 45 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 0cfa883..ed6f030 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -1310,6 +1310,44 @@ _asterisk_configure_do_cloud_firewall() { fi } +# ── Non-DO public VPS: no automated network-edge firewall step exists for +# arbitrary providers the way _asterisk_configure_do_cloud_firewall automates +# DigitalOcean via doctl -- there's no universal API to drive. But a box set +# up with a public FQDN is, in practice, almost always sitting behind some +# provider-managed firewall anyway, and skipping this reminder left it +# entirely unmentioned. Confirmed live on an IONOS VPS: UFW showed every SIP/ +# TURN/RTP port as ALLOW, Asterisk's own PJSIP logger showed zero incoming +# packets of any kind, and nothing in this installer's own output pointed at +# the actual cause -- IONOS's separate network-level firewall (Cloud Panel -> +# Networking -> Firewall Policies) only allowed 22/80/443/8443/8447 and +# silently dropped everything else before it ever reached the box. UFW being +# wide open proves nothing about a layer in front of it that UFW can't see. +_asterisk_remind_non_do_firewall() { + local WEB_ADMIN_PORT_VAL="$1" WEB_ADMIN_PUBLIC_ACCESS_NEEDED="$2" USE_EMBEDDED_COTURN_VAL="${3:-true}" + echo "" + log_warning "This box is reachable via FQDN but wasn't set up as a DigitalOcean droplet," + log_warning "so no automatic network-edge firewall was configured (that step only exists" + log_warning "for DO, via doctl). Most VPS/cloud providers run their OWN network-level" + log_warning "firewall in front of the box, separate from UFW and invisible to it — UFW can" + log_warning "show every port as ALLOW while traffic still gets silently dropped before it" + log_warning "ever reaches this box. Check your provider's console for it (e.g. IONOS: Cloud" + log_warning "Panel -> Networking -> Firewall Policies) and allow inbound, matching what UFW" + log_warning "just opened on this box:" + echo " TCP 22 (SSH)" + echo " UDP/TCP 5060 (SIP)" + echo " TCP 5061 (SIP TLS)" + [[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]] && echo " TCP ${WEB_ADMIN_PORT_VAL} (web admin)" + echo " TCP 8088, 8089 (Asterisk HTTP/HTTPS)" + echo " UDP 10000-20000 (RTP media)" + if [[ "$USE_EMBEDDED_COTURN_VAL" == true ]]; then + echo " UDP/TCP 3478 (TURN/STUN)" + echo " UDP 49152-49252 (TURN relay)" + else + echo " UDP/TCP 3478 and UDP 49152-49252 too, if the shared coturn instance" + echo " (services/coturn.sh) lives on this same box." + fi +} + # ── Shared: README ───────────────────────────────────────────────────────── # One document with a droplet-only section appended in public-cloud mode, so # the two deployment shapes can't document themselves differently by accident. @@ -1964,9 +2002,12 @@ ENV log_success "UFW rules added." fi - # ── DigitalOcean Cloud Firewall (network edge) ──────────────────────────── - [[ "$IS_DO" == true ]] && \ + # ── Network-edge firewall (in front of the box, not UFW) ────────────────── + if [[ "$IS_DO" == true ]]; then _asterisk_configure_do_cloud_firewall "$DROPLET_ID" "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" + elif [[ -n "$DOMAIN_NAME" ]]; then + _asterisk_remind_non_do_firewall "$WEB_ADMIN_PORT_VAL" "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" "$USE_EMBEDDED_COTURN" + fi # ── CrowdSec note ────────────────────────────────────────────────────────── # Not installed here — select it separately from the whiptail menu, or From b76230b341d40585164548a85cd80e4f75bcf147 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:33:24 +0000 Subject: [PATCH 3/3] asterisk: regenerate self-signed TLS cert when DOMAIN_NAME changes Vendor's entrypoint.sh only regenerates the self-signed cert if the file is missing or lacks a SAN extension -- it never checks whether the SAN actually matches the currently configured DOMAIN_NAME. Since /etc/asterisk/certs is a bind-mounted host directory, neither an update nor a full reinstall ever wipes it, so a domain entered once (even a placeholder, or one later changed) sticks in the cert indefinitely. Confirmed live: a box kept presenting a cert for a stale, originally- entered domain long after DOMAIN_NAME had changed and a full reinstall had run in between. Most SIP/TLS clients refuse a mismatched cert outright with no clear error, which was the actual cause of a "port's open but registration still fails" case -- firewall, coturn, and DNS had all already checked out clean. Patches the vendored entrypoint.sh (same guarded-sed pattern as the existing logger.conf patch) to also regenerate when the existing cert's SAN doesn't include the current DOMAIN_NAME. Verified against a scratch copy: missing cert regenerates, a cert already matching the domain is left alone, a mismatched domain now correctly regenerates and then stabilizes. --- services/asterisk.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/services/asterisk.sh b/services/asterisk.sh index ed6f030..d210cb1 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -385,6 +385,30 @@ _asterisk_refresh_vendor_files() { else log_warning "entrypoint.sh logger.conf template changed upstream — security events won't be logged to a file. Update the sed patch in this installer." fi + + # Regenerate the self-signed TLS cert when it doesn't match the current + # DOMAIN_NAME. Vendor's own check only asks "does the file exist" and + # "does it have a SAN extension" -- never "does the SAN match the domain + # actually configured now" -- so a domain entered once (even a + # placeholder, or one later changed) sticks in the cert FOREVER: it + # survives every subsequent update *and* full reinstall, because + # /etc/asterisk/certs is a bind-mounted host directory neither install + # mode ever wipes (the same reason pjsip.conf/devices survive reinstalls + # too). Confirmed live: a box's TLS transport kept presenting a cert for + # a stale, originally-entered domain long after DOMAIN_NAME had changed + # and a full reinstall had been run in between -- most SIP/TLS clients + # refuse a cert like that outright with no clear error, and this was the + # actual cause of a "port's open but registration still fails" case that + # every other check (firewall, coturn, DNS) had already come back clean. + if grep -q '^if \$regen_cert; then$' ./docker/entrypoint.sh; then + sed -i '/^if \$regen_cert; then$/i\ +if [[ "$regen_cert" != true && -n "${DOMAIN_NAME:-}" ]] && ! openssl x509 -in /etc/asterisk/certs/server.crt -noout -ext subjectAltName 2>/dev/null | grep -q "DNS:${DOMAIN_NAME}"; then\ + log_info "Existing TLS cert does not match current DOMAIN_NAME (${DOMAIN_NAME}) -- regenerating"\ + regen_cert=true\ +fi' ./docker/entrypoint.sh + else + log_warning "entrypoint.sh cert-regen check changed upstream — a stale-domain cert won't auto-regenerate. Update the sed patch in this installer." + fi } # ── Shared: log rotation for logs/full (unbounded otherwise) ──────────────