furtka-gateway/ops/firewall.sh
Robert Syrnicki 73c96cab56 Fix WireGuard peer routing and control-plane reachability
Two gaps found while walking through a real single-tenant deployment
scenario, both fixed and verified against the actual running stack (not
just unit tests):

- wg-easy moves to network_mode: host so Traefik can actually route to
  WireGuard peer addresses (wg-quick's route only existed inside wg-easy's
  own isolated network namespace before). This forced host-level sysctls
  (ops/host-sysctls.sh — Docker/runc rejects namespaced sysctls under host
  networking) and a host firewall rule (ops/firewall.sh) to keep wg-easy's
  now-unisolated admin API off the public interface.
- control-plane gets a real Traefik route (traefik/dynamic/control-plane.yml.example)
  instead of being reachable only at 127.0.0.1:8090 — box registration
  carries a WireGuard private key and needs TLS, not bare loopback HTTP.

Along the way, running the actual pinned wg-easy image
(ghcr.io/wg-easy/wg-easy:14) surfaced that it's a different, older,
Express-based codebase than what wg-easy's current docs/master branch
describe: auth is a plain Authorization header per request (no session
cookie), routes live under /api/wireguard/client, and its default
WG_ALLOWED_IPS is full-tunnel (0.0.0.0/0) rather than the split-tunnel this
design assumed. wgeasy.py and docker-compose.yaml are corrected
accordingly, verified end-to-end against the real container.

Also fixed: Docker Compose silently truncates a bcrypt hash's `$`
characters when read from .env — documented the required $$ escaping.
2026-08-24 12:51:22 +02:00

52 lines
2.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# Restrict wg-easy's admin API (tcp/51821) 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.
#
# 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
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."