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.
52 lines
2.5 KiB
Bash
Executable file
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."
|