|
Some checks failed
Build control-plane image / build-image (push) Has been cancelled
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 |
||
|---|---|---|
| .forgejo/workflows | ||
| control_plane | ||
| examples/starter-stack | ||
| ops | ||
| tests | ||
| traefik | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yaml | ||
| pyproject.toml | ||
| README.md | ||
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/ — 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_HOSTat (either one it already watches, or a new one you add as an additionalproviders.filedirectory 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:14and:15are confirmed supported (control_plane/wgeasy.pyandwgeasy_v15.pyrespectively — 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 fromops/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 withnetwork_mode: host, a macvlan, or explicit host routing that bridges its WireGuard interface out to wherever Traefik is. Verify this yourself before runningops/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:<some-port>(orping 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.shdoes this for you viadocker network connectafterdocker 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.
- Work through the Prerequisites checklist above.
- Run
ops/deploy.shand answer its prompts. - Add
GATEWAY_DYNAMIC_DIR_HOST(whatever path you gave the script) as aproviders.filedirectory on your Traefik, if it wasn't already watching it. - Point DNS: an A/AAAA record for
GATEWAY_CONTROL_PLANE_HOSTat 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'sbuild: ./control_planedoesn't work under Swarm at all — it only ever schedules pre-built, tagged images. Swarm falls back to theimage:key on the same service instead, which.forgejo/workflows/build-image.ymlkeeps published to this instance's container registry on every push tomaster(taggedlatestplus the short commit sha) — no manual build/tag step needed. SetGATEWAY_CONTROL_PLANE_IMAGEin.envfirst if you want a specific commit-sha tag rather than whateverlatestcurrently points to.docker stack deploydoes not read a.envfile next to the compose file the waydocker compose updoes —set -a; source .env; set +afirst, or the${VAR}substitutions come out empty.docker network connect(whatops/deploy.shuses 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 ownnetworks:section instead.depends_onis silently ignored by Swarm — harmless here, sincecontrol-planeonly talks to wg-easy/Traefik lazily on the first real request, not at startup.
Local dev
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