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
129 lines
6.6 KiB
Markdown
129 lines
6.6 KiB
Markdown
# 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:<some-port>` (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
|
|
```
|