2026-08-24 12:51:22 +02:00
|
|
|
#!/usr/bin/env bash
|
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
|
|
|
# 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
|
2026-08-24 12:51:22 +02:00
|
|
|
#
|
|
|
|
|
# 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
|
|
|
|
|
|
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
|
|
|
# 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}"
|
2026-08-24 12:51:22 +02:00
|
|
|
# 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."
|