diff --git a/CLAUDE.md b/CLAUDE.md index 03244f2..f99bb85 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,6 +159,52 @@ 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. + +### 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 169f5a8..80102ad 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -244,6 +244,55 @@ 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)." +} + +# 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 @@ -435,6 +484,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 +611,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 +646,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" diff --git a/services/asterisk-digital-ocean.sh b/services/asterisk-digital-ocean.sh index e45a2c2..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 @@ -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 diff --git a/services/asterisk.sh b/services/asterisk.sh index 418bd66..43cc7cd 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 + ufw_allow_from_caddy_net "${WEB_ADMIN_PORT_VAL}" + 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 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');