furtka-gateway/README.md
Robert Syrnicki 4a11d77297 docs: document new-server and existing Traefik/wg-easy setups
Root README now lets you choose between the two paths, with requirements,
the three ways to route Traefik to WireGuard peers (including the
bridge-network recipe verified on a live Traefik v3.7 + wg-easy v15),
box connection, troubleshooting and security notes. The starter-stack
README lists server, firewall and DNS requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:49:49 +02:00

8.8 KiB

furtka-gateway

Lets a Furtka box — typically at home behind NAT or CGNAT, with no public IP — publish individual apps on the internet under a real domain, with automatic HTTPS.

internet ──443──▶ Traefik ──WireGuard tunnel──▶ Furtka box (behind NAT) ──▶ app
                     ▲
Furtka box ──51820/udp──▶ wg-easy        (box dials out; no port forwarding at home)

A box connects as a WireGuard peer of wg-easy. Traefik then reverse-proxies HTTPS traffic through that tunnel to the apps the box owner has explicitly published. Only apps whose Furtka manifest declares internet.viable: true can be published at all, and each one is a separate opt-in on the box — the gateway never exposes anything by itself.

This repo's own code is the control-plane (control_plane/, one small stdlib-only Docker image). It creates WireGuard peers in wg-easy when a box registers, and writes Traefik file-provider routes when a box publishes an app.

Choose your setup

Option A: New server Option B: Existing Traefik + wg-easy
For An empty VPS A server that already runs Traefik and wg-easy
Runs wg-easy + Traefik + control-plane control-plane only
Setup examples/starter-stack/deploy.sh, 4 prompts ops/deploy.sh, plus one routing step you do yourself

Option A: New server

Full guide, including server and DNS requirements: examples/starter-stack/README.md.

In short: a Linux VPS with Docker, ports 80/tcp, 443/tcp, 51820/udp open, and a wildcard DNS record *.example.com pointing at it. Then:

git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
cd furtka-gateway/examples/starter-stack && ./deploy.sh

Option B: Existing Traefik + wg-easy

Requirements

  • Traefik v3 with a file provider directory (providers.file.directory). The control-plane writes one file per published app into it. If your Traefik uses providers.file.filename today, switch it to a directory and move your existing file into it — Traefik allows only one file provider.
  • wg-easy 14 or 15. Their admin APIs differ; you tell the deploy script which one you run. On v15, the control-plane sets wg-easy's default "Allowed IPs" for new clients to your peer subnet (split tunnel) on every start. Existing clients aren't touched, but new non-Furtka clients (phones, laptops) also get split tunnel unless you edit them.
  • Docker Compose v2.24+ on the server.
  • A route from Traefik to wg-easy's peer subnet (default 10.8.0.0/24) — see the next section. This is the one step you do yourself.
  • DNS: an A record for the gateway API hostname (e.g. gw.example.com) pointing at your Traefik, plus one per published app — or simply a wildcard *.example.com. Any registrar works; certificates come from your Traefik's existing resolver.
  • No new ports. Traefik's 443 and wg-easy's 51820/udp are already open.

Routing: Traefik must reach the WireGuard peers

Published routes point Traefik at a box's tunnel address (10.8.0.x). That address only exists inside wg-easy's network namespace, so a standard setup (wg-easy on a Docker bridge network) can't reach it. Pick whichever fits:

1. wg-easy on a bridge network (most common) — route the subnet through wg-easy and NAT it. Verified on a live Traefik v3.7 + wg-easy v15 setup.

a. Pin wg-easy's address on Traefik's network (in wg-easy's compose file), so the route below stays valid:

    networks:
      traefik:
        ipv4_address: 172.19.0.5   # any free address in that network's subnet

b. Route the peer subnet from the host to wg-easy, permanently (/etc/systemd/system/furtka-gateway-route.service):

[Unit]
Description=Route WireGuard peer subnet via wg-easy (furtka-gateway)
Requires=docker.service
After=docker.service
PartOf=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'for i in $(seq 90); do ip route replace 10.8.0.0/24 via 172.19.0.5 && exit 0; sleep 2; done; exit 1'
ExecStop=/sbin/ip route del 10.8.0.0/24 via 172.19.0.5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now furtka-gateway-route

c. Make replies come back through the tunnel: in wg-easy's admin UI (Admin → Hooks), append to PostUp

iptables -t nat -A POSTROUTING -s 172.19.0.0/16 -o wg0 -j MASQUERADE;

and to PostDown the same line with -D instead of -A (use your Traefik network's subnet). Then restart wg-easy once.

2. Traefik in wg-easy's network namespace — no routes or NAT at all; this is what Option A does. Only practical if you can restructure both containers.

3. wg-easy with network_mode: host — wg0 then lives on the host. Needs net.ipv4.ip_forward=1 on the host, and a firewall rule keeping wg-easy's admin port (51821) off the public interface, because host networking publishes it.

Check it, once a box is connected:

docker exec <your-traefik> ping -c1 10.8.0.<box>

Deploy

git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
cd furtka-gateway
ops/deploy.sh --render-only   # optional: inspect .env and rendered/control-plane.yml first
ops/deploy.sh

It asks for your wg-easy URL, version and login, your Traefik's dynamic directory, cert resolver and docker network, and the API hostname. It then builds and starts the control-plane on your Traefik's network, installs its route into the dynamic directory, and checks https://<api-host>/healthz.

Finding the answers:

Prompt Where to look
wg-easy URL http://<wg-easy container name>:51821 if it's on your Traefik's network
Version The tag of your wg-easy image (docker ps)
Dynamic directory The host path mounted at your Traefik's providers.file.directory
Cert resolver certificatesResolvers.<name> in your Traefik's static config
Docker network docker inspect <traefik> --format '{{json .NetworkSettings.Networks}}'

Update later with git pull && ops/deploy.sh --non-interactive. The image is always rebuilt from your checkout.

Connect a box

Same for both options. On the Furtka box: Settings → Gateway, enter the gateway URL (https://gw.example.com), the registration token (GATEWAY_BOX_TOKEN in the server's .env) and any box name. Then on the Apps page, open an app's ⋯ menu → Expose to Internet, and give it a hostname covered by your DNS.

Troubleshooting

Symptom Cause / fix
Box: Unit wg-quick@furtka-gw.service does not exist Box image lacks wireguard-tools. Install it (pacman -S wireguard-tools) or reinstall from a newer ISO, then click Connect again — the existing registration is reused.
Box says connected, but wg-easy shows no handshake Older box builds don't write wg-easy v15's preshared key into the tunnel config. Update the box.
App URL gives 502 / timeout, but the box's tunnel is up Traefik can't reach 10.8.0.x — see Routing.
No "Expose to Internet" in an app's menu That app's manifest lacks internet.viable: true.
deploy.sh: control-plane not healthy docker compose logs control-plane — usually a wrong wg-easy URL or password.
https://<api-host>/healthz fails after deploy DNS not pointing at Traefik yet, certificate still being issued, or Traefik isn't watching the dynamic directory.

Security notes

  • A box's registration response contains its WireGuard private key, so the API is only exposed through Traefik over HTTPS. The direct port is bound to 127.0.0.1:8090.
  • GATEWAY_BOX_TOKEN lets anyone register a box. Treat it like a password; rotate it by deleting it from .env and re-running the deploy script.
  • Keep wg-easy's admin UI (port 51821) off the public internet.

Docker Swarm (Option B only)

  • build: doesn't work under Swarm. It uses the image: CI publishes on every push to master (latest plus the short commit sha); set GATEWAY_CONTROL_PLANE_IMAGE in .env to pin one.
  • docker stack deploy doesn't read .env — set -a; source .env; set +a first.
  • Attach the service to your Traefik's network in the stack file's own networks: section (the generated docker-compose.override.yaml isn't used).
  • Option A's stack can't run under Swarm (network_mode: service:).

Development

python -m pytest            # control-plane tests
ruff check . && ruff format --check .

GATEWAY_MODE is a tenancy switch: single (one operator — everything above) or shared (multi-tenant; same code, not covered here).