furtka-gateway/examples/starter-stack/ops/firewall.sh
Robert Syrnicki e240f071d5
Some checks failed
Build control-plane image / build-image (push) Has been cancelled
feat: ship control-plane standalone, bring your own Traefik/wg-easy
Restructure the deployment model: instead of bundling and owning its own
Traefik + wg-easy, the control-plane now plugs into a Traefik and wg-easy
the operator already runs (as sibling containers, or otherwise reachable).
This avoids duplicating infra self-hosters commonly already have, and
sidesteps having to run a second ACME/DNS setup entirely — the operator's
own Traefik already has a working cert resolver.

- New control_plane/wgeasy_v15.py: a wg-easy v15 REST client, verified live
  against a real running container (auth model, route shapes, and the new
  per-client PresharedKey all differ materially from v14's API). Threaded
  the preshared key through to the box side (furtka/wireguard.py in the
  furtka repo) so v15's handshake actually completes.
- GATEWAY_CERT_RESOLVER is now a real env var instead of a GATEWAY_MODE-
  derived hardcoded "le", so it can mirror whatever resolver name (if any)
  the operator's own Traefik already has configured.
- Root docker-compose.yaml/ops/deploy.sh: control-plane only now. deploy.sh
  asks how the operator's Traefik reaches it (same docker network, joined
  via `docker network connect`, or a separate host) instead of prompting
  for DNS-01 providers and port remaps single-parallel mode needed.
- single-parallel mode is retired outright — nothing needs it once
  furtka-gateway doesn't own a second Traefik/wg-easy to remap ports for.
- The old bundled stack (own Traefik+wg-easy, HTTP-01) moves to
  examples/starter-stack/ as the documented zero-infra on-ramp.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3jVwAqESRjPdW9P9nvfem
2026-09-19 13:34:29 +02:00

60 lines
2.9 KiB
Bash
Executable file

#!/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."