# 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. The gateway's 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. Supports two deployment modes from the same codebase: - **`single`** — the common case: one person/operator runs this on their own VPS with their own domain's A/AAAA records pointed at it, for their own Furtka box(es). - **`shared`** — one larger, multi-tenant instance (operated by the Furtka project) for people who don't want to run their own. Same schema, same code; `single` is just the one-account case. 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. ## Deploying on a VPS 1. Get a VPS with a static public IP, point a domain's A/AAAA record at it (e.g. `example.com`), and open `80/tcp`, `443/tcp`, `51820/udp` in whatever firewall/security group sits in front of it. 2. `cp .env.example .env` and fill it in — in particular `GATEWAY_PUBLIC_HOST` (the WireGuard endpoint hostname), a wg-easy admin password (`WG_EASY_PASSWORD_HASH` + the matching plaintext `WG_EASY_ADMIN_PASSWORD`), `GATEWAY_BOX_TOKEN` (`openssl rand -hex 32`), and `GATEWAY_CONTROL_PLANE_HOST` (its own A/AAAA record — see step 5). **Double every `$` in `WG_EASY_PASSWORD_HASH` as `$$`** — see that variable's comment in `.env.example` for why; getting this wrong silently truncates the hash instead of erroring, confirmed by hitting it directly. 3. Run `sudo ops/host-sysctls.sh` on the VPS *before* bringing the stack up. wg-easy needs `net.ipv4.ip_forward=1` (so the host actually routes between the docker bridge and the WireGuard interface — see the `network_mode: host` comment on the `wg-easy` service) and `net.ipv4.conf.all.src_valid_mark`, and — confirmed by actually trying it, not assumed — Docker's own `sysctls:` key can't set either one once a service is host-networked; runc refuses to even start the container. 4. Bring the stack up: `docker compose up -d` (or `docker stack deploy` under Swarm — see the caveats below, `build:` doesn't work there yet). 5. Copy `traefik/dynamic/control-plane.yml.example` to `traefik/dynamic/control-plane.yml`, substituting your real `GATEWAY_CONTROL_PLANE_HOST`. Without this, the registration API is only reachable at `127.0.0.1:8090` on the VPS itself — a remote Furtka box has no way to reach it, and the call needs TLS anyway since it carries a WireGuard private key (see that file's comments, and `control_plane/wgeasy.py`'s module docstring, for why). 6. Run `sudo ops/firewall.sh` on the VPS. wg-easy's admin API is no longer kept off the public interface by Docker the way every other service here is (a direct consequence of `network_mode: host` — see step 3). This script closes that with an iptables rule. **Do this before pointing DNS at the box for real** — read the script's own caveats first, it hasn't been validated against a real multi-interface VPS yet. 7. `curl http://127.0.0.1:8090/healthz` from the VPS to confirm the stack is actually up. ### 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. Build and tag the image yourself first (`docker build -t furtka-gateway-control-plane:local ./control_plane`) and swap `build:` for `image: furtka-gateway-control-plane:local` until this is packaged as a versioned image in CI (the planned long-term fix). - `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. - `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 # GATEWAY_PUBLIC_HOST can be a LAN-reachable IP for a first smoke test. docker compose up -d curl http://127.0.0.1:8090/healthz ``` `ops/firewall.sh` and the `control-plane.yml` Traefik route are about reaching this gateway from the real internet — skip both for local dev.