bind nginx proxy manager to the tailscale address

It was the only service here publishing on 0.0.0.0, and a published docker port
is not behind the firewall: docker writes its DNAT rules straight into the nat
table, which ufw's INPUT chain never sees. `ufw default deny incoming` never
covered 80/443/81 — ufw-docker-rules.conf on the host exists to patch exactly
that, and patching a rule is weaker than never opening the socket.

The address is read from `tailscale ip -4` at run time rather than passed in,
because the host provisioning has already done `tailscale up` by the time this
executes. It is validated against 100.64.0.0/10, the range tailscale and
headscale both allocate from. SETUP_NPM_BIND overrides it.

With neither, selecting NPM exits instead of falling back to 0.0.0.0 — a
fallback would silently undo the point of the change.

Two consequences worth knowing. tailscaled becomes a boot-order dependency, so
the script warns when it is not enabled at boot; docker's restart policy covers
the window but only if the tailnet comes up on its own. And HTTP-01 ACME
challenges can no longer reach port 80, so any certificate NPM issues now needs
DNS-01.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 07:24:30 +02:00
co-authored by Claude Opus 5
parent 4e404f17c8
commit 2eedbcda54
+55 -3
View File
@@ -10,6 +10,8 @@
# Environment overrides: # Environment overrides:
# SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none") # SETUP_DOCKER_SERVICES="1 2 3" — pre-select services (or "all"/"none")
# SETUP_DOCKER_NETWORK="services" — docker network name # 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 set -e
@@ -47,6 +49,44 @@ prompt_value() {
# ─── docker network ───────────────────────────────────────────────────────── # ─── docker network ─────────────────────────────────────────────────────────
DOCKER_NETWORK="${SETUP_DOCKER_NETWORK:-services}" 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 # Ensure network exists
if ! docker network inspect "$DOCKER_NETWORK" &>/dev/null; then if ! docker network inspect "$DOCKER_NETWORK" &>/dev/null; then
docker network create "$DOCKER_NETWORK" >/dev/null 2>&1 docker network create "$DOCKER_NETWORK" >/dev/null 2>&1
@@ -94,6 +134,14 @@ MAILHOG_SELECTED=false
for svc in $SERVICES; do for svc in $SERVICES; do
case "$svc" in case "$svc" in
1) 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=<ip> 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") COMPOSE_SERVICES+=("nginx-proxy-manager")
cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
nginx-proxy-manager: nginx-proxy-manager:
@@ -101,9 +149,9 @@ for svc in $SERVICES; do
container_name: nginx-proxy-manager container_name: nginx-proxy-manager
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "$NPM_BIND:80:80"
- "443:443" - "$NPM_BIND:443:443"
- "81:81" - "$NPM_BIND:81:81"
volumes: volumes:
- ./npm_data:/data - ./npm_data:/data
- ./npm_letsencrypt:/etc/letsencrypt - ./npm_letsencrypt:/etc/letsencrypt
@@ -238,6 +286,10 @@ fi
# ─── output parseable values to stdout ─────────────────────────────────────── # ─── output parseable values to stdout ───────────────────────────────────────
echo "COMPOSE_DIR=$COMPOSE_DIR" echo "COMPOSE_DIR=$COMPOSE_DIR"
if [[ " ${COMPOSE_SERVICES[*]} " == *" nginx-proxy-manager "* ]]; then
echo "NPM_BIND=$NPM_BIND"
fi
if [[ -n "$PG_PASSWORD" ]]; then if [[ -n "$PG_PASSWORD" ]]; then
echo "POSTGRES_URL=postgresql://postgres:${PG_PASSWORD}@127.0.0.1:5432/${PG_DATABASE}" echo "POSTGRES_URL=postgresql://postgres:${PG_PASSWORD}@127.0.0.1:5432/${PG_DATABASE}"
fi fi