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.
wg-easy + Traefik docker-compose stack (Phase 1) plus a stdlib-only
control-plane API for box registration, WireGuard peer provisioning via
wg-easy, and per-box route publish/unpublish backed by Traefik's file
provider (Phase 2, single-tenant mode). SQLite holds accounts/boxes/routes
so a later multi-tenant shared instance is the same schema with more rows,
not a reshape.
wg-easy's actual REST API was verified against its source rather than
assumed: it has no bearer-token auth (session-cookie login via
POST /api/auth/password) and no way to accept an externally-generated
public key (it always mints the keypair itself, private key included) —
both corrected from the original plan during implementation.