diff --git a/scripts/setup/setup-dockers.sh b/scripts/setup/setup-dockers.sh index 34bd57c8..8e023e5d 100755 --- a/scripts/setup/setup-dockers.sh +++ b/scripts/setup/setup-dockers.sh @@ -10,6 +10,8 @@ # Environment overrides: # SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none") # SETUP_DOCKER_NETWORK="services" — docker network name +# SETUP_NPM_BIND="100.64.0.8" — host address Nginx Proxy Manager publishes on. Defaults to this +# node's Tailscale IPv4; set it explicitly to bind somewhere else. set -e @@ -47,6 +49,44 @@ prompt_value() { # ─── docker network ───────────────────────────────────────────────────────── DOCKER_NETWORK="${SETUP_DOCKER_NETWORK:-services}" +# ─── Nginx Proxy Manager bind address ─────────────────────────────────────── +# +# NPM is the only service here that ever published on 0.0.0.0, and a published Docker port is not +# behind the firewall: Docker writes its DNAT rules directly into the nat table, which UFW's INPUT +# chain never sees. `ufw default deny incoming` does not cover 80/443/81 — that is what the host's +# ufw-docker-rules.conf exists to patch, and patching a rule is weaker than never opening the socket. +# +# So bind to the tailnet address instead. The kernel then refuses the socket on every other interface +# and the firewall stops being load-bearing for this. The address is read at run time rather than +# passed in, because by the time this script runs the host provisioning has already done `tailscale up`. +resolve_npm_bind() { + if [[ -n "${SETUP_NPM_BIND:-}" ]]; then + echo "$SETUP_NPM_BIND" + return + fi + local ip + ip=$(tailscale ip -4 2>/dev/null | head -1) + # 100.64.0.0/10 — the CGNAT range both Tailscale and Headscale allocate from. Anything outside it + # means `tailscale ip` answered with something unexpected, and a bind address is not a value to + # guess at: the whole point is that it is NOT reachable from the internet. + if [[ "$ip" =~ ^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\. ]]; then + echo "$ip" + return + fi + echo "" +} + +NPM_BIND="$(resolve_npm_bind)" + +# Binding to an address that belongs to another service's interface makes that service a boot-order +# dependency: if tailscaled has not brought tailscale0 up yet, the container cannot get its socket and +# Docker falls back on the restart policy to retry. That converges, but only if the tailnet comes up +# at all on its own. +if [[ -n "$NPM_BIND" ]] && ! systemctl is-enabled --quiet tailscaled 2>/dev/null; then + warn "tailscaled is not enabled at boot — NPM binds $NPM_BIND, which will not exist after a reboot" + warn "until the tailnet is up. Fix with: sudo systemctl enable tailscaled" +fi + # Ensure network exists if ! docker network inspect "$DOCKER_NETWORK" &>/dev/null; then docker network create "$DOCKER_NETWORK" >/dev/null 2>&1 @@ -94,6 +134,14 @@ MAILHOG_SELECTED=false for svc in $SERVICES; do case "$svc" in 1) + if [[ -z "$NPM_BIND" ]]; then + fail "Nginx Proxy Manager selected, but no Tailscale IPv4 was found on this host." + echo " Bring the tailnet up first (the host provisioning does this), or choose the" >&2 + echo " address deliberately: SETUP_NPM_BIND= bash scripts/setup/setup-dockers.sh" >&2 + echo " Publishing it on 0.0.0.0 is not offered — Docker bypasses UFW, so that would put" >&2 + echo " 80/443/81 on every interface the host has." >&2 + exit 1 + fi COMPOSE_SERVICES+=("nginx-proxy-manager") cat >> "$COMPOSE_DIR/docker-compose.yaml" <