# furtka-gateway WireGuard (wg-easy) + Traefik reverse-proxy that lets a Furtka box — typically sitting behind NAT/CGNAT with no public IP — expose individual apps to the internet under a real domain with automatic TLS. A Furtka box becomes a WireGuard peer of this gateway. Your Traefik reverse-proxies public HTTPS traffic over that tunnel to whichever apps the box owner has explicitly published. Only apps whose Furtka `manifest.json` declares `internet.viable: true` can be published at all, and publishing is always an explicit per-app opt-in on the box side — the gateway never exposes anything on its own. ## What this actually is The control-plane (`control_plane/`, one small stdlib-only Docker image) is all this repo ships. It does two things: talks to wg-easy's admin API to provision WireGuard peers for boxes, and writes plain Traefik file-provider YAML for the routes those boxes publish. It does not own or bundle a Traefik or wg-easy of its own — **you plug it into a Traefik and wg-easy you already run** (a very common self-hosting pair), the same way you'd plug any other self-hosted app into your existing reverse proxy. This also means your Traefik's own cert resolver (whatever you already use) just works for these new routes too — furtka-gateway doesn't set up its own ACME/DNS story at all. Don't already run Traefik or wg-easy? See [`examples/starter-stack/`](examples/starter-stack/) — a zero-infra on-ramp that bundles both. `GATEWAY_MODE` is purely a tenancy switch: `single` (one operator, the common case) or `shared` (one larger, multi-tenant instance — same schema, same code, out of scope for the walkthrough below). It has no effect on ports, certs, or which Traefik/wg-easy you use — that's all just your own infrastructure now. Status: **Phase 2** — the control plane (accounts/boxes/routes, wg-easy peer provisioning, Traefik dynamic-config generation) is built and tested for single-tenant mode. The Furtka-core side (the box-facing "connect to a gateway" flow and per-app "expose to internet" toggle) is still just a plan, not yet implemented — see the plan doc for the phased delivery. ## Prerequisites checklist Read this before running `ops/deploy.sh` — a couple of these can't be verified from the script itself, and a deploy that skips them fails in confusing ways later. - **A running Traefik**, with a file-provider directory you can point `GATEWAY_DYNAMIC_DIR_HOST` at (either one it already watches, or a new one you add as an *additional* `providers.file` directory in its static config — additive, doesn't touch your existing dynamic config). - **A running wg-easy.** Know its version — `ghcr.io/wg-easy/wg-easy:14` and `:15` are confirmed supported (`control_plane/wgeasy.py` and `wgeasy_v15.py` respectively — these are genuinely different, incompatible APIs, not just version numbers; each module's own docstring documents exactly what was verified, live, against a real container). Anything else is unverified. - **Traefik must have an actual routed network path to wg-easy's WireGuard peer subnet** (default `10.8.0.0/24`). This is the one thing that can't be automated or checked from `ops/deploy.sh` — it depends entirely on how you've deployed the two. A plain bridge-networked wg-easy container (Docker's default) will **not** work: `wg0`'s route only exists inside wg-easy's own network namespace unless wg-easy runs with `network_mode: host`, a macvlan, or explicit host routing that bridges its WireGuard interface out to wherever Traefik is. Verify this yourself before running `ops/deploy.sh`: register a test box (or use any existing WireGuard peer's address), then from Traefik's container, `wget -qO- http://10.8.0.x:` (or `ping 10.8.0.x`) should succeed. If it doesn't, fix your wg-easy/Traefik network setup first — nothing in this repo can route around it. - **How Traefik reaches the control-plane container itself.** The recommended path is joining your Traefik's own docker network ( `ops/deploy.sh` does this for you via `docker network connect` after `docker compose up`); a separate-host setup is also supported but needs its own access control — see the script's prompts. ## Deploying **`ops/deploy.sh`** prompts for all of the above, generates `GATEWAY_BOX_TOKEN`/`GATEWAY_ADMIN_TOKEN`, writes `.env`, renders `traefik/dynamic/control-plane.yml` from its tracked template, brings the control-plane up, joins your Traefik's network (if that's your choice), and health-checks it. Safe to re-run — an existing `.env`'s values become the defaults for each prompt. 1. Work through the Prerequisites checklist above. 2. Run `ops/deploy.sh` and answer its prompts. 3. Add `GATEWAY_DYNAMIC_DIR_HOST` (whatever path you gave the script) as a `providers.file` directory on your Traefik, if it wasn't already watching it. 4. Point DNS: an A/AAAA record for `GATEWAY_CONTROL_PLANE_HOST` at wherever your Traefik is reachable. Run `ops/deploy.sh --render-only` to just (re)write `.env` and the rendered `control-plane.yml` without touching anything live — useful to inspect what would be generated, or after hand-editing `.env`. ### Docker Swarm caveats If deploying via `docker stack deploy` instead of `docker compose up`: - `docker-compose.yaml`'s `build: ./control_plane` doesn't work under Swarm at all — it only ever schedules pre-built, tagged images. Swarm falls back to the `image:` key on the same service instead, which `.forgejo/workflows/build-image.yml` keeps published to this instance's container registry on every push to `master` (tagged `latest` plus the short commit sha) — no manual build/tag step needed. Set `GATEWAY_CONTROL_PLANE_IMAGE` in `.env` first if you want a specific commit-sha tag rather than whatever `latest` currently points to. - `docker stack deploy` does not read a `.env` file next to the compose file the way `docker compose up` does — `set -a; source .env; set +a` first, or the `${VAR}` substitutions come out empty. - `docker network connect` (what `ops/deploy.sh` uses to join your Traefik's network) is a plain compose/Docker concept, not Swarm-specific — under Swarm, attach the service to your Traefik's network directly in the stack file's own `networks:` section instead. - `depends_on` is silently ignored by Swarm — harmless here, since `control-plane` only talks to wg-easy/Traefik lazily on the first real request, not at startup. ## Local dev ```bash cp .env.example .env # fill in WG_EASY_URL etc. for whatever Traefik/wg-easy you're testing # against locally, or use examples/starter-stack/ for a fully bundled # local smoke test. docker compose up -d curl http://127.0.0.1:8090/healthz ```