From 9f2a3ddfd954749dd832f46064a80eeef30e9f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:24:02 +0000 Subject: [PATCH] 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