#!/usr/bin/env bash # Restrict wg-easy's admin API (tcp/51821, or GATEWAY_WG_ADMIN_PORT if # overridden) to loopback and the gateway's own docker bridge network. Run # this once on the VPS after `docker compose up -d`, and again if the # "internal" network's subnet ever changes. # # `sudo` does not inherit your shell's env or read .env for you — if you # overrode GATEWAY_WG_ADMIN_PORT, run: # set -a; source .env; set +a; sudo -E ops/firewall.sh # # Why this exists: wg-easy runs with network_mode: host (see # docker-compose.yaml's comment on why — Traefik needs a route to # WireGuard peer addresses that only exists in whatever network namespace # wg-easy's wg0 interface lives in). Host networking means Docker itself # can no longer keep wg-easy's admin port off the VPS's public interface # the way it does for every other container here — this script is that # missing piece, done with iptables instead. # # NOT yet validated against a real multi-interface VPS. Review # DOCKER_BRIDGE_SUBNET against your actual `docker network inspect # furtka-gateway_internal` output before relying on this as your only line # of defense. A cloud provider security group that blocks 51821/tcp # entirely is a good belt-and-suspenders addition on top of this — wg-easy's # admin API has no legitimate reason to ever be reached from outside this # host. # # IPv4 only. If this VPS also has a public IPv6 address, ip6tables needs # the equivalent rules added by hand — not yet handled here. set -euo pipefail # Must match GATEWAY_WG_ADMIN_PORT in .env if you overrode it (e.g. # GATEWAY_MODE=single-parallel, where 51821 is already taken by another # wg-easy on this host). ADMIN_PORT="${GATEWAY_WG_ADMIN_PORT:-51821}" # Must match docker-compose.yaml's networks.internal.ipam.config subnet. DOCKER_BRIDGE_SUBNET="172.28.0.0/24" if [[ $EUID -ne 0 ]]; then echo "must be run as root" >&2 exit 1 fi # Insert ACCEPT rules at the head of the chain, then append DROP at the # tail — this ordering holds regardless of how many times the script runs, # since -I always lands above whatever's already there (including a # previous run's DROP) and -A always lands below. iptables -C INPUT -p tcp --dport "$ADMIN_PORT" -s 127.0.0.1 -j ACCEPT 2>/dev/null \ || iptables -I INPUT -p tcp --dport "$ADMIN_PORT" -s 127.0.0.1 -j ACCEPT iptables -C INPUT -p tcp --dport "$ADMIN_PORT" -s "$DOCKER_BRIDGE_SUBNET" -j ACCEPT 2>/dev/null \ || iptables -I INPUT -p tcp --dport "$ADMIN_PORT" -s "$DOCKER_BRIDGE_SUBNET" -j ACCEPT iptables -C INPUT -p tcp --dport "$ADMIN_PORT" -j DROP 2>/dev/null \ || iptables -A INPUT -p tcp --dport "$ADMIN_PORT" -j DROP echo "wg-easy admin API (tcp/$ADMIN_PORT) now restricted to loopback + $DOCKER_BRIDGE_SUBNET" echo "NOTE: these rules do not persist across reboot on most distros —" echo "install iptables-persistent (Debian/Ubuntu) or an equivalent, or add" echo "this script to a boot-time hook."