- Always build from the checkout (`up --build`, `pull_policy: build`)
instead of reusing a stale local image or pulling an old registry one.
- --render-only writes to rendered/ and no longer touches the live
Traefik dynamic directory; the route is installed after health check.
- Join the Traefik network via a generated docker-compose.override.yaml
rather than `docker network connect` after startup, which raced the
control-plane's startup call to wg-easy and was lost on recreate.
- Add --non-interactive, check the public HTTPS endpoint, and point the
closing instructions at Settings -> Gateway (the suggested
`furtka gateway connect` command doesn't exist).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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.