deploy-paths #1
2 changed files with 304 additions and 183 deletions
309
README.md
309
README.md
|
|
@ -1,129 +1,206 @@
|
||||||
# furtka-gateway
|
# furtka-gateway
|
||||||
|
|
||||||
WireGuard (wg-easy) + Traefik reverse-proxy that lets a Furtka box — typically
|
Lets a Furtka box — typically at home behind NAT or CGNAT, with no public IP —
|
||||||
sitting behind NAT/CGNAT with no public IP — expose individual apps to the
|
publish individual apps on the internet under a real domain, with automatic
|
||||||
internet under a real domain with automatic TLS.
|
HTTPS.
|
||||||
|
|
||||||
A Furtka box becomes a WireGuard peer of this gateway. Your Traefik
|
```
|
||||||
reverse-proxies public HTTPS traffic over that tunnel to whichever apps the
|
internet ──443──▶ Traefik ──WireGuard tunnel──▶ Furtka box (behind NAT) ──▶ app
|
||||||
box owner has explicitly published. Only apps whose Furtka `manifest.json`
|
▲
|
||||||
declares `internet.viable: true` can be published at all, and publishing is
|
Furtka box ──51820/udp──▶ wg-easy (box dials out; no port forwarding at home)
|
||||||
always an explicit per-app opt-in on the box side — the gateway never exposes
|
```
|
||||||
anything on its own.
|
|
||||||
|
|
||||||
## What this actually is
|
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.
|
||||||
|
|
||||||
The control-plane (`control_plane/`, one small stdlib-only Docker image) is
|
This repo's own code is the **control-plane** (`control_plane/`, one small
|
||||||
all this repo ships. It does two things: talks to wg-easy's admin API to
|
stdlib-only Docker image). It creates WireGuard peers in wg-easy when a box
|
||||||
provision WireGuard peers for boxes, and writes plain Traefik file-provider
|
registers, and writes Traefik file-provider routes when a box publishes an app.
|
||||||
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
|
## Choose your setup
|
||||||
[`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
|
| | [Option A: New server](#option-a-new-server) | [Option B: Existing Traefik + wg-easy](#option-b-existing-traefik--wg-easy) |
|
||||||
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
|
| For | An empty VPS | A server that already runs Traefik and wg-easy |
|
||||||
ports, certs, or which Traefik/wg-easy you use — that's all just your own
|
| Runs | wg-easy + Traefik + control-plane | control-plane only |
|
||||||
infrastructure now.
|
| Setup | `examples/starter-stack/deploy.sh`, 4 prompts | `ops/deploy.sh`, plus one routing step you do yourself |
|
||||||
|
|
||||||
Status: **Phase 2** — the control plane (accounts/boxes/routes, wg-easy
|
## Option A: New server
|
||||||
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
|
Full guide, including server and DNS requirements:
|
||||||
|
**[examples/starter-stack/README.md](examples/starter-stack/README.md)**.
|
||||||
|
|
||||||
Read this before running `ops/deploy.sh` — a couple of these can't be
|
In short: a Linux VPS with Docker, ports `80/tcp`, `443/tcp`, `51820/udp`
|
||||||
verified from the script itself, and a deploy that skips them fails in
|
open, and a wildcard DNS record `*.example.com` pointing at it. Then:
|
||||||
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
|
```bash
|
||||||
cp .env.example .env
|
git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
|
||||||
# fill in WG_EASY_URL etc. for whatever Traefik/wg-easy you're testing
|
cd furtka-gateway/examples/starter-stack && ./deploy.sh
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[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
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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](examples/starter-stack/) 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <your-traefik> ping -c1 10.8.0.<box>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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](#routing-traefik-must-reach-the-wireguard-peers). |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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).
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,117 @@
|
||||||
# furtka-gateway starter stack
|
# furtka-gateway on a new server
|
||||||
|
|
||||||
Zero-infra on-ramp: bundles its own Traefik + wg-easy alongside the
|
Everything in one Docker Compose stack: **wg-easy** (WireGuard), **Traefik**
|
||||||
control-plane, for an operator who doesn't already run either. This is a
|
(HTTPS reverse proxy with Let's Encrypt), and the **furtka-gateway
|
||||||
secondary path — if you already have Traefik and/or wg-easy running
|
control-plane**. Use this on a server that doesn't run anything else on
|
||||||
(a common self-hosting setup), use the main repo's root `docker-compose.yaml`
|
ports 80/443. If you already run Traefik and wg-easy, see
|
||||||
+ `ops/deploy.sh` instead, which plugs into what you already have rather
|
[the root README](../../README.md#option-b-existing-traefik--wg-easy) instead.
|
||||||
than running a second copy. See the root `README.md`'s Prerequisites
|
|
||||||
checklist.
|
|
||||||
|
|
||||||
This stack always runs in `GATEWAY_MODE=single`, owns ports `80/tcp`,
|
```
|
||||||
`443/tcp`, and `51820/udp` on the host directly, and uses HTTP-01 certs
|
internet ──443──▶ Traefik ──WireGuard tunnel──▶ Furtka box (behind NAT) ──▶ app
|
||||||
(no DNS provider needed) — the tradeoffs that make `single-parallel` mode
|
▲
|
||||||
necessary elsewhere don't apply here, because this stack assumes nothing
|
Furtka box ──51820/udp──▶ wg-easy (box dials out; no port forwarding at home)
|
||||||
else is already using those ports.
|
|
||||||
|
|
||||||
## Deploying on a VPS
|
|
||||||
|
|
||||||
1. Get a VPS with a static public IP, and point two A/AAAA records at it:
|
|
||||||
one for `GATEWAY_PUBLIC_HOST` (the WireGuard endpoint) and one for
|
|
||||||
`GATEWAY_CONTROL_PLANE_HOST` (the registration API — needs its own
|
|
||||||
hostname since the response carries a WireGuard private key in
|
|
||||||
plaintext JSON, so that call must go over HTTPS).
|
|
||||||
2. Run `./deploy.sh` from this directory and answer its prompts. It
|
|
||||||
generates the wg-easy admin password hash, `GATEWAY_BOX_TOKEN`, and
|
|
||||||
`GATEWAY_ADMIN_TOKEN`, writes `.env`, renders
|
|
||||||
`traefik/static/traefik.active.yml` and
|
|
||||||
`traefik/dynamic/control-plane.yml` from their tracked templates, runs
|
|
||||||
`sudo ops/host-sysctls.sh` (wg-easy needs `net.ipv4.ip_forward=1` and
|
|
||||||
`net.ipv4.conf.all.src_valid_mark` — see that script's comments for why
|
|
||||||
Docker's own `sysctls:` key can't set these under `network_mode: host`),
|
|
||||||
brings the stack up, runs `sudo ops/firewall.sh` (locks wg-easy's admin
|
|
||||||
API off the public interface — `network_mode: host` means Docker itself
|
|
||||||
can no longer do that automatically), and health-checks it.
|
|
||||||
3. Open `80/tcp`, `443/tcp`, `51820/udp` in whatever firewall/security
|
|
||||||
group sits in front of the VPS.
|
|
||||||
|
|
||||||
Run `./deploy.sh --render-only` to just (re)write `.env` and the rendered
|
|
||||||
Traefik files without touching sysctls/compose/firewall — useful to inspect
|
|
||||||
what would be generated, or after hand-editing `.env`.
|
|
||||||
|
|
||||||
## Local dev
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy.sh --render-only
|
|
||||||
# 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
|
## Server requirements
|
||||||
reaching this gateway from the real internet — skip both for local dev.
|
|
||||||
|
|
||||||
## Docker Swarm caveats
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Machine | Any Linux VPS, x86_64 or arm64. 1 vCPU / 512 MB RAM is plenty. |
|
||||||
|
| Public IP | A static public IPv4 address. |
|
||||||
|
| Kernel | WireGuard support (Linux 5.6+ — every current distro). |
|
||||||
|
| Software | Docker Engine with the Compose plugin **v2.24 or newer** (`docker compose version`), plus `git`, `curl`, `openssl`. |
|
||||||
|
| Free ports | `80/tcp`, `443/tcp`, `51820/udp` — nothing else may use them. |
|
||||||
|
| Firewall | Allow those three ports inbound, in the server's firewall *and* any provider firewall/security group. Nothing else needs to be open. |
|
||||||
|
|
||||||
If deploying via `docker stack deploy` instead of `docker compose up`:
|
Installing Docker on a fresh Debian/Ubuntu server:
|
||||||
- `docker-compose.yaml`'s `build: ../../control_plane` doesn't work under
|
|
||||||
Swarm at all — it only ever schedules pre-built, tagged images. Swarm
|
```bash
|
||||||
falls back to the `image:` key on the same service instead, which CI
|
curl -fsSL https://get.docker.com | sh
|
||||||
keeps published to the registry on every push to `master` (tagged
|
```
|
||||||
`latest` plus the short commit sha). Set `GATEWAY_CONTROL_PLANE_IMAGE` in
|
|
||||||
`.env` first if you want a specific commit-sha tag rather than whatever
|
## DNS requirements
|
||||||
`latest` currently points to.
|
|
||||||
- `docker stack deploy` does not read a `.env` file next to the compose
|
Easiest: **one wildcard record** for your domain, pointing at the server.
|
||||||
file the way `docker compose up` does — `set -a; source .env; set +a`
|
|
||||||
first, or the `${VAR}` substitutions come out empty. **Do not `source`
|
| Type | Name | Value |
|
||||||
`.env` directly if `WG_EASY_PASSWORD_HASH` is set** — bash expands `$$`
|
|---|---|---|
|
||||||
(its own PID) while sourcing, silently corrupting the hash's doubled-`$`
|
| A | `*.example.com` | your server's IPv4 |
|
||||||
escaping. Export that one variable separately, unescaped, or set it
|
|
||||||
directly on the `docker stack deploy` command line.
|
That covers everything at once:
|
||||||
- `depends_on` is silently ignored by Swarm — harmless here, since
|
|
||||||
`control-plane` only talks to wg-easy/Traefik lazily on the first real
|
- `vpn.example.com` — the WireGuard endpoint boxes dial
|
||||||
request, not at startup.
|
- `gw.example.com` — the gateway API you enter on a box
|
||||||
|
- `<app>.example.com` — every app a box publishes later, with no DNS change per app
|
||||||
|
|
||||||
|
No wildcard possible? Create A records for `vpn.` and `gw.`, plus one per app
|
||||||
|
before you publish it. Certificates use Let's Encrypt's HTTP challenge, so no
|
||||||
|
DNS provider API is needed — any registrar works.
|
||||||
|
|
||||||
|
If you also add AAAA (IPv6) records, the server must be reachable over IPv6 on
|
||||||
|
the same three ports.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://forgejo.sourcegate.online/robert/furtka-gateway.git
|
||||||
|
cd furtka-gateway/examples/starter-stack
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It asks for four things — your domain, the two hostnames (defaults
|
||||||
|
`vpn.<domain>` and `gw.<domain>`), and an email for Let's Encrypt — then
|
||||||
|
generates all passwords and tokens into `.env`, starts the stack, checks DNS
|
||||||
|
and health, and prints what to enter on a box.
|
||||||
|
|
||||||
|
`./deploy.sh --render-only` writes `.env` and `dynamic/control-plane.yml`
|
||||||
|
without starting anything. `./deploy.sh --non-interactive` takes every value
|
||||||
|
from an existing `.env`, for scripted installs.
|
||||||
|
|
||||||
|
## Connect a box
|
||||||
|
|
||||||
|
On the Furtka box: **Settings → Gateway**, enter
|
||||||
|
|
||||||
|
- **Gateway URL:** `https://gw.example.com`
|
||||||
|
- **Registration token:** `GATEWAY_BOX_TOKEN` from `.env`
|
||||||
|
- **Box name:** anything
|
||||||
|
|
||||||
|
Then publish an app from the Apps page: **⋯ → Expose to Internet**, and give it
|
||||||
|
a hostname like `tools.example.com`.
|
||||||
|
|
||||||
|
## Day to day
|
||||||
|
|
||||||
|
| Task | Command (in this folder) |
|
||||||
|
|---|---|
|
||||||
|
| Update | `git pull && ./deploy.sh --non-interactive` |
|
||||||
|
| Logs | `docker compose logs -f control-plane` (or `wg-easy`, `traefik`) |
|
||||||
|
| Restart | `docker compose restart` |
|
||||||
|
| wg-easy admin UI | `ssh -L 51821:127.0.0.1:51821 <server>`, then open http://localhost:51821 — user `admin`, password `WG_EASY_ADMIN_PASSWORD` from `.env` |
|
||||||
|
| Backup | `.env`, `dynamic/`, and the Docker volumes `*_wg_easy_data`, `*_control_plane_data`, `*_traefik_acme` |
|
||||||
|
|
||||||
|
Changed the wg-easy password in its UI? Put the new one in `.env` as
|
||||||
|
`WG_EASY_ADMIN_PASSWORD` and run `./deploy.sh --non-interactive`.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
wg-easy creates the `wg0` interface and the route to the boxes
|
||||||
|
(`10.8.0.0/24`) inside its own network namespace. Traefik must be in that same
|
||||||
|
namespace to reach a box's tunnel address. So both join one namespace owned by
|
||||||
|
`gateway-net`, a do-nothing container whose only job is to hold the namespace
|
||||||
|
and publish the ports. Because it never restarts, wg-easy and Traefik can each
|
||||||
|
restart independently without cutting the other off. No host routes, NAT rules,
|
||||||
|
sysctls or host networking are needed, and wg-easy's admin UI is never
|
||||||
|
published to the internet.
|
||||||
|
|
||||||
|
Two consequences:
|
||||||
|
|
||||||
|
- **Restart through Compose.** `docker compose restart` handles everything.
|
||||||
|
If `furtka-gateway-net` itself was restarted with a bare `docker restart`,
|
||||||
|
wg-easy and Traefik lose their network — fix with `docker compose restart`
|
||||||
|
(plain `docker compose up -d` does *not* repair it).
|
||||||
|
- **Compose only, not Swarm.** `network_mode: service:` isn't supported by
|
||||||
|
`docker stack deploy`.
|
||||||
|
|
||||||
|
Verified with a simulated box: route publishing, plus restarts of wg-easy,
|
||||||
|
Traefik and `gateway-net`, and a full stack stop/start. A real server reboot
|
||||||
|
hasn't been tested yet — after your first reboot, check that
|
||||||
|
`https://gw.example.com/healthz` answers.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue