From 2330b47dd9b8ccac17af0683493c29b296129518 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:20:23 +0000 Subject: [PATCH 1/5] Add shared helpers: Caddy-fronting signal, safe UFW enable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit configure_caddy_for_service() previously gave callers no way to know whether Caddy actually ended up fronting the service, or whether that was local (reachable only over host.docker.internal) vs remote (needs network access to this host). Services that also open a host firewall port for the same thing had no way to correctly skip that when Caddy is the only intended way in. Now sets CADDY_SERVICE_CONFIGURED/CADDY_SERVICE_MODE out-params after each exit point. Added ensure_ufw_enabled(): flips UFW from inactive to active (no service in this repo has ever done this — ufw allow rules just sat unenforced). Always allows SSH first, reading the real port from sshd_config in case it's non-default, so this can't lock out the session running the installer. No-ops if UFW is already active. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- CLAUDE.md | 28 ++++++++++++++++++++++++++++ lib/common.sh | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 03244f2..308a347 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,6 +159,34 @@ reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument is an optional string inserted verbatim inside the Caddy site block (use it for `import authelia` or custom matchers). +Sets two out-params (not `local` — read them after the call returns) so the +caller can tell whether Caddy actually ended up fronting the service: + +```bash +CADDY_SERVICE_CONFIGURED # true/false +CADDY_SERVICE_MODE # "local" or "remote" (only meaningful if configured) +``` + +Use this to skip opening a host firewall port for a service Caddy already +fronts *locally* (it reaches the service over `host.docker.internal`, not +the network) — but still open it when `CADDY_SERVICE_MODE` is `"remote"`, +since a remote Caddy machine needs to reach this host over the network +instead. See `services/asterisk.sh` and `services/asterisk-digital-ocean.sh` +for the reference pattern: call `configure_caddy_for_service` *before* +building firewall rules, not after, so the decision is known in time. + +### UFW enable + +```bash +ensure_ufw_enabled +``` + +Call this **after** your service has already added its own `ufw allow` +rules — it only flips UFW from inactive to active, it doesn't add rules for +you. No-ops if UFW is already active or not installed. Always allows SSH +first (reading the real port from `sshd_config` in case it's non-default) +before enabling, so this can't lock out the session running the installer. + ### README generation ```bash diff --git a/lib/common.sh b/lib/common.sh index 169f5a8..7373440 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -244,6 +244,30 @@ ensure_caddy_network() { && log_info "Created Docker network ${_net} (needed by Caddy-fronted services)" } +# Enables UFW if it isn't already active. Call this AFTER the caller has +# already added its own `ufw allow` rules for whatever it needs — this only +# flips UFW from inactive to active, it doesn't add rules for the calling +# service itself. +# +# Always allows SSH first, using the sshd_config port if it's non-default — +# getting this wrong and then enabling UFW would lock out the very SSH +# session most people are running this script from. If UFW is already +# active, this is a no-op (assumed already handled correctly). +ensure_ufw_enabled() { + command -v ufw &>/dev/null || return 0 + [ "$DRY_RUN" = true ] && return 0 + ufw status 2>/dev/null | grep -q "Status: active" && return 0 + + local _ssh_port + _ssh_port="$(grep -iE '^[[:space:]]*Port[[:space:]]+[0-9]+' /etc/ssh/sshd_config 2>/dev/null \ + | tail -1 | awk '{print $2}')" + _ssh_port="${_ssh_port:-22}" + + ufw allow "${_ssh_port}/tcp" comment 'SSH' >/dev/null 2>&1 + ufw --force enable >/dev/null 2>&1 + log_success "UFW enabled (SSH on port ${_ssh_port} allowed first, so this won't lock you out)." +} + # ── SSH client config (~/.ssh/config) Host aliases ──────────────────────────── # Lets "ssh " connect directly to user@host without typing it out each # time — handy for VPN/NetBird peers with unmemorable IPs. Operates on the @@ -435,6 +459,16 @@ write_readme() { configure_caddy_for_service() { local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}" + # Out-params (not `local` — callers read these after the call returns) so + # a caller can tell whether Caddy actually ended up fronting the service + # and, if so, whether that's a local container (reachable only over the + # host's internal network) or a remote machine (needs to reach this host + # over the network — usually its public IP). Services that also open a + # host firewall for the same port use this to skip that when Caddy is + # already the only intended way in, instead of leaving both routes open. + CADDY_SERVICE_CONFIGURED=false + CADDY_SERVICE_MODE="" + # Derive the proxy upstream and a port number for display messages. # Plain number → host.docker.internal:PORT (host-network or legacy # services — Caddy itself runs in its own container on @@ -552,11 +586,16 @@ CADDY_BLOCK local OVERWRITE="" prompt_yn "Overwrite existing configuration? (y/n):" "n" OVERWRITE if [ "$OVERWRITE" != "y" ] && [ "$OVERWRITE" != "Y" ]; then - echo " Keeping existing configuration."; return 0 + echo " Keeping existing configuration." + CADDY_SERVICE_CONFIGURED=true + CADDY_SERVICE_MODE="local" + return 0 fi sed -i "/^${SERVICE_DOMAIN}/,/^}/d" "$CADDYFILE" fi + CADDY_SERVICE_CONFIGURED=true + CADDY_SERVICE_MODE="local" echo " Adding $SERVICE_NAME configuration to Caddyfile..." printf '%s\n' "$_SITE_BLOCK" >> "$CADDYFILE" @@ -582,6 +621,8 @@ CADDY_BLOCK # ── Remote Caddy: write snippet file ───────────────────────────────────── else + CADDY_SERVICE_CONFIGURED=true + CADDY_SERVICE_MODE="remote" local SNIPPET_DIR="$DOCKER_DIR/caddy-snippets" local SNIPPET_FILE="$SNIPPET_DIR/${DEFAULT_SUBDOMAIN}.caddy" mkdir -p "$SNIPPET_DIR" From 516e0db66fcb438144c66b3c4f33832dbc4b7713 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:20:30 +0000 Subject: [PATCH 2/5] asterisk.sh: same port-exposure fix and UFW enable as the DO edition Mirrors the fixes just made in asterisk-digital-ocean.sh: - Reordered so the Caddy reverse-proxy decision happens before the UFW rules are built, using the new CADDY_SERVICE_CONFIGURED/ CADDY_SERVICE_MODE signal from configure_caddy_for_service() to skip opening the web admin port on the LAN when a local Caddy is already fronting it (still opens it for a remote Caddy machine, which needs LAN access to reach this host directly). - Calls the new ensure_ufw_enabled() so UFW actually enforces the rules this script adds, instead of leaving them queued but inert. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- services/asterisk.sh | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 418bd66..45d0b88 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -503,23 +503,11 @@ WEB_ADMIN_AUTH_DISABLED=false ENV chmod 600 .env - # ── UFW firewall rules ──────────────────────────────────────────────────── - if command -v ufw &>/dev/null; then - log_info "Opening UFW ports for Asterisk + coturn..." - ufw allow 5060/udp - ufw allow 5060/tcp - ufw allow 5061/tcp - ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" - ufw allow 8088/tcp - ufw allow 8089/tcp - ufw allow 3478/udp - ufw allow 3478/tcp - ufw allow 10000:20000/udp - ufw allow 49152:49252/udp - log_success "UFW rules added." - fi - # ── Caddy reverse proxy for web admin ───────────────────────────────────── + # Decided before the firewall rules below so they can be scoped + # correctly: if a local Caddy ends up fronting the web admin, there's no + # reason to also expose it on the LAN — Caddy already reaches it over + # the host's internal network (host.docker.internal). local EXTRA_BLOCK="" if [ -d "$DOCKER_DIR/authelia" ]; then local _use_auth="" @@ -532,6 +520,28 @@ ENV fi configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK" + # ── UFW firewall rules ──────────────────────────────────────────────────── + if command -v ufw &>/dev/null; then + log_info "Opening UFW ports for Asterisk + coturn..." + ufw allow 5060/udp + ufw allow 5060/tcp + ufw allow 5061/tcp + if [[ "$CADDY_SERVICE_CONFIGURED" == true && "$CADDY_SERVICE_MODE" == "local" ]]; then + ufw delete allow "${WEB_ADMIN_PORT_VAL}/tcp" 2>/dev/null || true + log_info "Web admin port ${WEB_ADMIN_PORT_VAL} kept off the LAN — Caddy fronts it locally." + else + ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" + fi + ufw allow 8088/tcp + ufw allow 8089/tcp + ufw allow 3478/udp + ufw allow 3478/tcp + ufw allow 10000:20000/udp + ufw allow 49152:49252/udp + ensure_ufw_enabled + log_success "UFW rules added." + fi + # ── README ──────────────────────────────────────────────────────────────── write_readme "$EA_DIR" << 'MD' # Easy Asterisk PBX + coturn From 67ab1737bfcbf365b29554de43307552bca2e934 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:20:34 +0000 Subject: [PATCH 3/5] asterisk-digital-ocean.sh: enable UFW after adding its rules Calls the new ensure_ufw_enabled() so the UFW rules this script adds actually get enforced instead of sitting queued but inactive. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- services/asterisk-digital-ocean.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/services/asterisk-digital-ocean.sh b/services/asterisk-digital-ocean.sh index e45a2c2..f32bb4c 100755 --- a/services/asterisk-digital-ocean.sh +++ b/services/asterisk-digital-ocean.sh @@ -707,6 +707,7 @@ CADDY_BLOCK ufw allow 3478/tcp ufw allow 10000:20000/udp ufw allow 49152:49252/udp + ensure_ufw_enabled log_success "UFW rules added." fi From cc63d51d24ef2f36945a7dc1f7789d1a835daf47 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:20:41 +0000 Subject: [PATCH 4/5] Default new devices to mobile category and TLS transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both defaulted to whatever sorted/listed first (kiosks category, LAN/VPN UDP transport) — reasonable for a fixed intercom install, but the common case here is adding a phone over the internet. Default the category select to "mobile" specifically (not just first-in-list, so it survives category reordering) and make FQDN/Internet (TLS) the default transport option instead of LAN/VPN (UDP). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- vendor/easy-asterisk/easy-asterisk-v0.10.0.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh index 4aad948..4aaba9c 100755 --- a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh +++ b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh @@ -5076,8 +5076,8 @@ HTML_TEMPLATE = '''
@@ -5314,9 +5314,11 @@ HTML_TEMPLATE = ''' devicesCache = devices; renderDevices(); - // Update category select in add device form + // Update category select in add device form. Most devices + // added day-to-day are phones, not fixed kiosks, so default + // to the "mobile" category instead of whichever sorts first. document.getElementById('category-select').innerHTML = categoriesCache.map(c => - `` + `` ).join(''); } catch (e) { showAlert('Failed to load devices', 'error'); @@ -5455,7 +5457,7 @@ HTML_TEMPLATE = ''' }).join(''); document.getElementById('category-select').innerHTML = categories.map(c => - `` + `` ).join(''); } catch (e) { showAlert('Failed to load categories', 'error'); From 9f2a3ddfd954749dd832f46064a80eeef30e9f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:24:02 +0000 Subject: [PATCH 5/5] Fix: closing the web admin port to the internet also blocked Caddy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed live: a bare `ufw delete allow ` closes it on every interface, including the caddy_net bridge — Caddy's own request to host.docker.internal:PORT is ordinary INPUT-chain traffic as far as UFW is concerned, not something that bypasses it just because the source is a local container. Closing the port outright silently took Caddy's reverse-proxy path down with it. Added ufw_allow_from_caddy_net() to scope the port to caddy_net's own subnet instead of leaving it fully closed — reachable from Caddy, still closed to the public internet. Wired into both asterisk-digital-ocean.sh and asterisk.sh in place of the plain delete. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- CLAUDE.md | 18 ++++++++++++++++++ lib/common.sh | 25 +++++++++++++++++++++++++ services/asterisk-digital-ocean.sh | 2 +- services/asterisk.sh | 2 +- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 308a347..f99bb85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,6 +187,24 @@ you. No-ops if UFW is already active or not installed. Always allows SSH first (reading the real port from `sshd_config` in case it's non-default) before enabling, so this can't lock out the session running the installer. +### Closing a port to the internet without also closing it to Caddy + +```bash +ufw_allow_from_caddy_net PORT [PROTO] # PROTO defaults to tcp +``` + +When `CADDY_SERVICE_MODE` is `"local"` (see above) and you `ufw delete +allow` a port because Caddy fronts it now, don't stop there — UFW rules +apply to *all* interfaces unless scoped, and Caddy's own request to +`host.docker.internal:PORT` is ordinary INPUT-chain traffic arriving over +the `caddy_net` bridge, not the public internet. A bare `ufw delete allow` +blocks that too and silently breaks the service (confirmed live: closing +the web admin port outright took Caddy down with it). Call +`ufw_allow_from_caddy_net` right after the `delete` to re-open the port +scoped to just `caddy_net`'s subnet — reachable from Caddy, not from the +internet. See `services/asterisk-digital-ocean.sh` and +`services/asterisk.sh` for the pattern. + ### README generation ```bash diff --git a/lib/common.sh b/lib/common.sh index 7373440..80102ad 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -268,6 +268,31 @@ ensure_ufw_enabled() { log_success "UFW enabled (SSH on port ${_ssh_port} allowed first, so this won't lock you out)." } +# Scopes a UFW allow rule to just the caddy_net bridge subnet instead of +# every interface. Needed for any port that only needs to be reachable from +# a *locally* Caddy-fronted service (via host.docker.internal) — a plain +# `ufw delete allow ` closes it everywhere, but Caddy's own request to +# host.docker.internal is still ordinary INPUT-chain traffic as far as UFW +# is concerned, arriving over the caddy_net bridge, not the internet. UFW +# rules apply to all interfaces unless scoped like this, so closing the +# port outright also silently breaks Caddy. +ufw_allow_from_caddy_net() { + local _port="$1" _proto="${2:-tcp}" + command -v ufw &>/dev/null || return 0 + [ "$DRY_RUN" = true ] && return 0 + + local _subnet + _subnet="$(docker network inspect "${SITE_CADDY_NET:-caddy_net}" \ + --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null)" + if [ -n "$_subnet" ]; then + ufw allow from "$_subnet" to any port "$_port" proto "$_proto" comment 'Caddy internal only' >/dev/null 2>&1 + log_info "Port ${_port}/${_proto} reachable only from Caddy's internal network (${_subnet}), not the public internet." + else + log_warning "Could not determine ${SITE_CADDY_NET:-caddy_net}'s subnet — port ${_port}/${_proto} stays closed." + log_warning "If Caddy can't reach it: ufw allow from to any port ${_port} proto ${_proto}" + fi +} + # ── SSH client config (~/.ssh/config) Host aliases ──────────────────────────── # Lets "ssh " connect directly to user@host without typing it out each # time — handy for VPN/NetBird peers with unmemorable IPs. Operates on the diff --git a/services/asterisk-digital-ocean.sh b/services/asterisk-digital-ocean.sh index f32bb4c..6282c44 100755 --- a/services/asterisk-digital-ocean.sh +++ b/services/asterisk-digital-ocean.sh @@ -699,7 +699,7 @@ CADDY_BLOCK ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" else ufw delete allow "${WEB_ADMIN_PORT_VAL}/tcp" 2>/dev/null || true - log_info "Web admin port ${WEB_ADMIN_PORT_VAL} kept closed to the internet — Caddy fronts it locally." + ufw_allow_from_caddy_net "${WEB_ADMIN_PORT_VAL}" fi ufw allow 8088/tcp ufw allow 8089/tcp diff --git a/services/asterisk.sh b/services/asterisk.sh index 45d0b88..43cc7cd 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -528,7 +528,7 @@ ENV ufw allow 5061/tcp if [[ "$CADDY_SERVICE_CONFIGURED" == true && "$CADDY_SERVICE_MODE" == "local" ]]; then ufw delete allow "${WEB_ADMIN_PORT_VAL}/tcp" 2>/dev/null || true - log_info "Web admin port ${WEB_ADMIN_PORT_VAL} kept off the LAN — Caddy fronts it locally." + ufw_allow_from_caddy_net "${WEB_ADMIN_PORT_VAL}" else ufw allow "${WEB_ADMIN_PORT_VAL}/tcp" fi