From 2330b47dd9b8ccac17af0683493c29b296129518 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 18:20:23 +0000 Subject: [PATCH] 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"