docs: Remote-Access-Gateway-Design (v1, user-hosted) #16
1 changed files with 362 additions and 0 deletions
362
docs/remote-access.md
Normal file
362
docs/remote-access.md
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
# Remote access — user-hosted gateway design
|
||||||
|
|
||||||
|
**Status: Draft for review — Daniel/Robert meeting 2026-08-24.**
|
||||||
|
Supersedes the remote-access paths of [wizard-flow.md](wizard-flow.md) S5/S6
|
||||||
|
(the NS-delegation / `*.furtka.cloud` design) for v1; the managed-DNS ideas
|
||||||
|
there move to a later hosted phase (see §9). Builds on Robert's proxy-stack
|
||||||
|
proposal from 2026-04-18.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goals & non-goals
|
||||||
|
|
||||||
|
**Goal:** a Furtka box owner can reach their apps from anywhere at
|
||||||
|
`https://jellyfin.example.de` — real Let's Encrypt certificates, no browser
|
||||||
|
warnings, no port forwarding on the home router, no CGNAT pain — while the
|
||||||
|
whole chain stays self-hosted end to end (DSGVO/ethos: no central Furtka
|
||||||
|
service in the traffic path).
|
||||||
|
|
||||||
|
The UX bar is the usual one: *dein Vater soll das einrichten können* — with
|
||||||
|
the one honest caveat that v1 requires renting a cheap VPS and owning a
|
||||||
|
domain. That caveat is what the later managed phase removes.
|
||||||
|
|
||||||
|
**Non-goals for v1 (explicit OUT list):**
|
||||||
|
|
||||||
|
- Furtka-operated gateway hosting (managed/multi-tenant *operation*)
|
||||||
|
- Furtka-run DNS (`ns1/ns2.furtka.org`, NS delegation, DNS-01 wildcards)
|
||||||
|
- Authentik / SSO / forward-auth (§7 keeps the door open)
|
||||||
|
- Exposing the Furtka admin UI itself
|
||||||
|
- Multi-box setups behind one gateway
|
||||||
|
- Non-HTTP protocols (SMB, MQTT — TCP passthrough is a later Caddy `layer4`
|
||||||
|
exercise; SMB never goes on the public internet)
|
||||||
|
- Road-warrior WireGuard profiles for family devices (the WG server will
|
||||||
|
already be there; one obvious future step)
|
||||||
|
- E-mail/MX anything, metrics dashboards, HA/failover
|
||||||
|
|
||||||
|
## 2. Inherited decisions
|
||||||
|
|
||||||
|
Decided 2026-08-23/24 (Daniel):
|
||||||
|
|
||||||
|
1. **Hybrid trajectory.** v1 is a stack the *user* hosts on their own VPS.
|
||||||
|
The same stack must be designed so Furtka could later operate it as a
|
||||||
|
hosted offering — multi-tenant *capable* in design (§9), not built.
|
||||||
|
2. **Bring-your-own domain in v1.** The user points a wildcard A/AAAA record
|
||||||
|
at their VPS. No Furtka DNS involvement.
|
||||||
|
3. Deliverable of this step: this document + the phased plan (§11).
|
||||||
|
|
||||||
|
**Relationship to Robert's 2026-04-18 proposal** — the shape survives intact
|
||||||
|
(user-hosted compose stack on a VPS, WireGuard tunnel, pairing code, per-app
|
||||||
|
subdomains). Three deliberate changes, argued in this doc:
|
||||||
|
|
||||||
|
| Proposal | This design | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Traefik | Caddy (§3) | consistency with the box, On-Demand TLS fits BYO-domain perfectly, Traefik's label-config advantage doesn't apply (apps run on the box, not the VPS) |
|
||||||
|
| Three components (proxy + WG + web/router glue) | Two: Caddy + `gatewayd` (§3) | the "pairing web page" is a TOFU/phishing trap on a not-yet-certed IP; the VPS console is the correct trust anchor |
|
||||||
|
| VPS *displays* code on a page, user enters VPS address first | code printed by the install command / `gatewayctl code`; one screen, two fields (domain + code) | fewer round trips, no public pairing page, respects the "max 3 technical things per page" UX rule |
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
Two deployable sides:
|
||||||
|
|
||||||
|
```
|
||||||
|
user's VPS (public IP, wildcard DNS) Furtka box (home LAN, NAT/CGNAT)
|
||||||
|
┌──────────────────────────────────┐ ┌────────────────────────────────┐
|
||||||
|
│ furtka-gateway (docker compose) │ │ │
|
||||||
|
│ │ │ wg-quick@furtka-gw (client) │
|
||||||
|
│ ┌────────┐ admin api ┌─────┐ │ WireGuard│ 10.88.x.y/32 │
|
||||||
|
│ │ caddy │◄────/load────│gate-│ │◄═════════│ │
|
||||||
|
│ │ :443 │──ask────────►│wayd │ │ tunnel │ apps publish host ports │
|
||||||
|
│ └────────┘ └─────┘ │ (box │ (jellyfin :8096, HA :8123, …) │
|
||||||
|
│ │ │ │ initiates│ │
|
||||||
|
│ │ reverse_proxy │state│ outbound)│ furtka/remote.py │
|
||||||
|
│ ▼ ▼ │ │ /var/lib/furtka/remote.json │
|
||||||
|
│ 10.88.x.y:8096 state/tenants/ │ │ nftables rules on wg iface │
|
||||||
|
└──────────────────────────────────┘ └────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Caddy** (official image) terminates TLS on `:443`/`:80`. Certificates per
|
||||||
|
subdomain via **On-Demand TLS**: no wildcard cert, no DNS-01, no registrar
|
||||||
|
API — Caddy mints `jellyfin.example.de` on first request, gated by an `ask`
|
||||||
|
endpoint served by gatewayd from the tenant/route state. Unknown hostnames
|
||||||
|
are refused *before* ACME fires, which doubles as cert-mint DoS protection.
|
||||||
|
- **gatewayd** — one small stdlib-only Python service (same zero-dependency
|
||||||
|
discipline as `furtka/`): pairing endpoint, route sync, WireGuard peer
|
||||||
|
management (`wg` via subprocess, NET_ADMIN), Caddyfile rendering pushed via
|
||||||
|
Caddy's admin API (`POST /load`) on the internal compose network. No docker
|
||||||
|
socket mount. gatewayd re-pushes config on boot, so a Caddy restart or
|
||||||
|
container recreation converges. Ships a `gatewayctl` CLI (`code`, `status`,
|
||||||
|
`revoke <tenant>`).
|
||||||
|
- **Box side**: new `furtka/remote.py` + `furtka remote` CLI + settings card.
|
||||||
|
The box is a plain WireGuard *client* (`wg-quick@furtka-gw`) and initiates
|
||||||
|
everything — outbound-only, `PersistentKeepalive = 25` — so NAT and CGNAT
|
||||||
|
are non-issues and the gateway never needs to reach into the LAN uninvited.
|
||||||
|
|
||||||
|
**Data path.** `https://jellyfin.example.de` → VPS Caddy (TLS ends here) →
|
||||||
|
plain HTTP over the WG tunnel (WG is the encryption layer) →
|
||||||
|
`<box-wg-ip>:8096`. This works **today** with zero box networking changes:
|
||||||
|
every web app in the catalog already publishes a host port (it-tools 8080,
|
||||||
|
home-assistant 8123, jellyfin 8096, uptime-kuma 3001, zigbee2mqtt 8084) — the
|
||||||
|
missing shared app network (dependency gap #2) does not block remote access
|
||||||
|
and stays uncoupled. Caddy's `reverse_proxy` handles WebSockets natively
|
||||||
|
(Home Assistant needs them) and sets `X-Forwarded-For`/`X-Forwarded-Proto`.
|
||||||
|
Evolution note: once box-side per-app routing exists, the gateway can target
|
||||||
|
the box's Caddy `:80` with the Host header preserved instead of raw ports —
|
||||||
|
the route-map schema (§5) does not change.
|
||||||
|
|
||||||
|
**Privacy default:** VPS access logs are off or IP-anonymized by default —
|
||||||
|
cheap to do, and a genuine differentiator for the audience.
|
||||||
|
|
||||||
|
## 4. Pairing protocol
|
||||||
|
|
||||||
|
**Trust anchor: DNS first, then real TLS.** Pairing requires the wildcard
|
||||||
|
(or at least `gw.<domain>`) record to be live, because the install command
|
||||||
|
allowlists `gw.<domain>` immediately — so the box's very first contact is
|
||||||
|
over a genuine Let's Encrypt certificate. No TOFU, no fingerprint-comparison
|
||||||
|
ceremony. The pairing code is the second, independent factor: it proves
|
||||||
|
console access to the VPS *and* cryptographically binds the key exchange.
|
||||||
|
|
||||||
|
Code properties: printed by `install.sh` (re-printable via `gatewayctl
|
||||||
|
code`), one-time, 10-minute TTL, ~48 bits formatted `K7PF-2Q9M-X3RD`,
|
||||||
|
invalidated after 5 failed attempts, verified in constant time, rate-limited.
|
||||||
|
|
||||||
|
```
|
||||||
|
VPS console gatewayd (VPS) Furtka box
|
||||||
|
----------- -------------- ----------
|
||||||
|
1. user runs install.sh --> gen WG server keypair,
|
||||||
|
--domain example.de allowlist gw.example.de,
|
||||||
|
print pairing code C (TTL 10m)
|
||||||
|
2. user opens Furtka /settings, enters domain "example.de" + code C
|
||||||
|
3. resolve gw.example.de,
|
||||||
|
gen WG keypair,
|
||||||
|
POST https://gw.example.de/v1/pair
|
||||||
|
{box_pubkey, box_name,
|
||||||
|
mac = HMAC(C, box_pubkey)}
|
||||||
|
4. verify mac with C (one-time,
|
||||||
|
constant-time, rate-limited);
|
||||||
|
mint tenant_id + bearer token;
|
||||||
|
add WG peer; assign tunnel IPs
|
||||||
|
--> 200 {tenant_id, token, gw_pubkey,
|
||||||
|
wg_endpoint, box_ip, gw_ip, domain,
|
||||||
|
mac = HMAC(C, gw_pubkey || box_pubkey)}
|
||||||
|
5. verify response mac
|
||||||
|
(kills a TLS-MITM key swap);
|
||||||
|
write /var/lib/furtka/remote.json (0600)
|
||||||
|
+ wg config (0600);
|
||||||
|
systemctl enable --now wg-quick@furtka-gw
|
||||||
|
6. WG handshake (box initiates,
|
||||||
|
keepalive 25s — NAT-safe)
|
||||||
|
7. PUT /v1/routes (Bearer): full map
|
||||||
|
8. render Caddyfile,
|
||||||
|
POST caddy admin /load
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Re-pair**: new code; replaces the WG peer and revokes the old token.
|
||||||
|
- **Unpair (box side)**: `DELETE /v1/tenant`, wipe `remote.json`, disable the
|
||||||
|
wg unit.
|
||||||
|
- **Revoke (gateway side, stolen-box case)**: `gatewayctl revoke <tenant>` —
|
||||||
|
removes token, peer, and routes.
|
||||||
|
- **State**: box → `/var/lib/furtka/remote.json`, mode 0600, written with the
|
||||||
|
tmp→chmod→replace idiom from `furtka/auth.py:save_users`. VPS → one JSON
|
||||||
|
file per tenant (`state/tenants/<id>.json`, atomic replace). Not sqlite:
|
||||||
|
n=1 in v1, trivially debuggable and backupable, and it matches the
|
||||||
|
file-state idiom used everywhere else in Furtka. sqlite (stdlib anyway) is
|
||||||
|
the documented escape hatch if the managed phase needs it.
|
||||||
|
|
||||||
|
## 5. Box ↔ gateway API
|
||||||
|
|
||||||
|
All endpoints HTTPS on `gw.<domain>`, JSON bodies,
|
||||||
|
`Authorization: Bearer <token>` except `/v1/pair`. **Only the box ever
|
||||||
|
initiates; there is no gateway→box API at all.**
|
||||||
|
|
||||||
|
| Endpoint | Auth | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST /v1/pair` | pairing code (HMAC) | key exchange, tenant + token mint |
|
||||||
|
| `PUT /v1/routes` | Bearer | **full desired-state** route map — idempotent, replay-harmless; gateway rejects `revision` ≤ last applied |
|
||||||
|
| `GET /v1/status` | Bearer | gateway version, api_version, WG last-handshake, per-route cert state — feeds the settings card |
|
||||||
|
| `DELETE /v1/tenant` | Bearer | unpair: remove peer, routes, state |
|
||||||
|
|
||||||
|
Route map shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"revision": 17,
|
||||||
|
"routes": [
|
||||||
|
{"subdomain": "jellyfin", "port": 8096, "auth": "builtin"},
|
||||||
|
{"subdomain": "tools", "port": 8080, "auth": "basic",
|
||||||
|
"basic_user": "daniel", "basic_hash": "$2b$..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Versioning: `/v1/` path prefix; every response echoes `api_version`; the
|
||||||
|
gateway supports current and current−1; the box refuses to sync against an
|
||||||
|
unknown major and surfaces "update your gateway" in the settings card.
|
||||||
|
|
||||||
|
## 6. Manifest `expose` + per-app UX
|
||||||
|
|
||||||
|
New **optional** manifest object; apps without it get no public toggle:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"expose": {
|
||||||
|
"subdomain": "jellyfin",
|
||||||
|
"port": 8096,
|
||||||
|
"auth": "builtin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `subdomain` — default public name, user-overridable in the UI.
|
||||||
|
- `port` — the *host* port the gateway proxies to over the tunnel. Explicit
|
||||||
|
on purpose: `ports` stays informational (its ordering is ambiguous —
|
||||||
|
it-tools maps 8080:80, zigbee2mqtt 8084:8080), and `open_url` stays a
|
||||||
|
display template, not a routing contract.
|
||||||
|
- `auth` — `"builtin"` (the app has its own login: jellyfin, home-assistant,
|
||||||
|
uptime-kuma) or `"none"` (it-tools).
|
||||||
|
|
||||||
|
**Backward compatible by construction**: `furtka/manifest.py` reads known
|
||||||
|
keys and ignores unknown ones, so old cores parse new catalog manifests
|
||||||
|
untouched; `expose` is additive and opt-in per app.
|
||||||
|
|
||||||
|
**Per-app toggle, default OFF for every app** (DSGVO + attack surface).
|
||||||
|
Toggling triggers a full `PUT /v1/routes`. Apps with `auth: "none"` get a
|
||||||
|
hard warning ("Diese App hat keinen eigenen Login — wirklich öffentlich
|
||||||
|
machen?") plus a one-click gateway **Basic Auth** option: username + bcrypt
|
||||||
|
hash generated on the box, only the hash ships in the route map.
|
||||||
|
|
||||||
|
**`open_url` interaction**: when the box is paired and an app is exposed, the
|
||||||
|
API rewrites that app's link server-side to `https://<subdomain>.<domain>/`
|
||||||
|
(around the manifest serialization in `furtka/api.py`, `_manifest_summary`).
|
||||||
|
This also finally unifies today's divergent client-side substitution
|
||||||
|
(`furtka/api.py:353` uses `location.hostname`;
|
||||||
|
`assets/www/index.html:107-118` uses the furtka.json hostname).
|
||||||
|
|
||||||
|
## 7. Auth in v1, and the Authentik path
|
||||||
|
|
||||||
|
v1 relies on **app-native logins + guardrails**:
|
||||||
|
|
||||||
|
1. `auth: "builtin"` apps expose cleanly — their own login is the gate (the
|
||||||
|
same posture as every competitor, see [competitors.md](competitors.md)).
|
||||||
|
2. `auth: "none"` apps require the explicit warning and offer gateway Basic
|
||||||
|
Auth (Caddy `basic_auth` directive).
|
||||||
|
|
||||||
|
Forward-auth against the box's session auth (`furtka/auth.py`) is **rejected
|
||||||
|
for v1**: cookie-domain gymnastics across subdomains, a per-request round
|
||||||
|
trip over the tunnel, and it couples gateway availability to the box API.
|
||||||
|
|
||||||
|
**Future-proofing that costs nothing now**: the route-map `auth` field is an
|
||||||
|
enum `none | basic | forward`. When Authentik (locked in
|
||||||
|
[wizard-flow.md](wizard-flow.md) as the IdP, still unbuilt) lands on the box,
|
||||||
|
the gateway receives `forward` routes pointing at a box-side outpost — zero
|
||||||
|
gateway redesign.
|
||||||
|
|
||||||
|
## 8. Threat model
|
||||||
|
|
||||||
|
| Asset / scenario | Attacker capability | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| MITM at pairing | on-path between box and VPS | DNS-first requirement → real LE TLS at first contact; HMAC-with-code binds *both* WG pubkeys in request and response; code is one-time, 10-min TTL, console-only |
|
||||||
|
| **Compromised VPS** (the big one — it's a cheap VPS) | TLS plaintext of exposed apps; a WG path toward the LAN | **non-negotiable v1 scope: box-side nftables on the WG interface** — inbound restricted to exactly the exposed ports, no forwarding, peer `AllowedIPs` = gateway /32. Without this, VPS compromise = LAN pivot. (The box has no firewall code today; this feature is where it must grow some.) |
|
||||||
|
| Stolen/compromised box | valid token + WG key | `gatewayctl revoke <tenant>` kills token, peer, routes from the VPS side |
|
||||||
|
| Replay | captured pair/sync requests | one-time TTL'd codes; idempotent full-state PUTs with monotonic `revision` |
|
||||||
|
| Cert-mint DoS | mass requests for random subdomains | On-Demand TLS `ask` allowlist + per-IP rate limit — ACME never fires for unknown names |
|
||||||
|
| Malicious co-tenant (managed phase) | claims another tenant's hostname | hostname→tenant resolution in `ask`; a tenant's domain is recorded at pairing and cross-tenant claims are refused |
|
||||||
|
| Exposed app with no login | anyone on the internet | default-OFF toggles; `auth:none` warning; Basic Auth option |
|
||||||
|
|
||||||
|
## 9. Multi-tenant-capable design constraints
|
||||||
|
|
||||||
|
Baked in from day one (cheap now, expensive to retrofit):
|
||||||
|
|
||||||
|
- `tenant_id` on all gateway state and log lines; bearer token per tenant
|
||||||
|
- WG tunnel IPs allocated from a pool (`10.88.0.0/16`, one /32 per box)
|
||||||
|
- `ask` endpoint resolves hostname→tenant and refuses cross-tenant claims
|
||||||
|
- versioned `/v1/` API; structured JSON logs
|
||||||
|
- rate-limit configuration *shaped* per-tenant (v1 values are global)
|
||||||
|
|
||||||
|
Explicitly **not** built in v1: tenant provisioning UI, billing, Furtka-run
|
||||||
|
DNS/NS delegation, per-tenant network namespaces, HA/failover, metrics
|
||||||
|
endpoint. The managed offering later is "Furtka operates this same stack +
|
||||||
|
the DNS story from wizard-flow.md S5" — not a second codebase.
|
||||||
|
|
||||||
|
## 10. Repos, artifacts, box-side changes
|
||||||
|
|
||||||
|
**New Forgejo repo `daniel/furtka-gateway`:**
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose.yml # official caddy image + gatewayd image
|
||||||
|
gatewayd/ # stdlib-only Python 3 (~500 lines), Dockerfile
|
||||||
|
install.sh # --domain example.de; prints pairing code
|
||||||
|
docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
- Releases: CalVer + sha256 sidecar + release.json, Forgejo Actions on the
|
||||||
|
self-hosted runner — cloned from `scripts/build-release-tarball.sh` /
|
||||||
|
`scripts/publish-release.sh` / the `furtka/_release_common.py` shape.
|
||||||
|
- Install UX: `curl -fsSL https://…/install.sh | bash -s -- --domain
|
||||||
|
example.de`, with the paranoid path (download, read, verify sha256, run)
|
||||||
|
documented right next to it.
|
||||||
|
- gatewayd tests follow the `tests/test_https.py` style: inject paths and a
|
||||||
|
fake subprocess/reload via kwargs.
|
||||||
|
|
||||||
|
**Box side (core repo):**
|
||||||
|
|
||||||
|
- `furtka/remote.py` — pattern donors: `furtka/https.py` (atomic write +
|
||||||
|
reload + rollback, error class contract), `furtka/catalog.py` +
|
||||||
|
`furtka/_release_common.py` (HTTP, state JSON), `furtka/paths.py` accessor
|
||||||
|
for `remote.json`
|
||||||
|
- `furtka remote pair|status|sync|unpair` in `furtka/cli.py`
|
||||||
|
- `_do_remote_*` handlers + dispatch lines in `furtka/api.py` (same shape as
|
||||||
|
the HTTPS pair at `api.py:1032/1039`)
|
||||||
|
- settings card "Remote access" (replaces the dead `furtka.org/#planned`
|
||||||
|
links — 10 occurrences across `assets/www/`)
|
||||||
|
- `wireguard-tools` added to the archinstall package list
|
||||||
|
(`webinstaller/app.py` `packages`, ~line 453) and the wg-quick unit wired
|
||||||
|
via `_FURTKA_UNITS` (`webinstaller/app.py:261`); upgraded boxes get it via
|
||||||
|
`updater._link_new_units`
|
||||||
|
- nftables rules on the WG interface (see §8)
|
||||||
|
|
||||||
|
**Subdomain scheme**: `app.domain.tld` in v1; `gw.<domain>` reserved for the
|
||||||
|
gateway itself. Route-sync rejects reserved names: `gw`, `furtka`, `www`,
|
||||||
|
`mail`, `autoconfig`, `autodiscover`, `_acme-challenge`. Multi-box later:
|
||||||
|
`app.<boxname>.domain.tld` — reserved here, zero code now. The Furtka admin
|
||||||
|
UI is not exposed in v1; `furtka.<domain>` stays reserved for a later opt-in
|
||||||
|
once a stronger auth story exists.
|
||||||
|
|
||||||
|
## 11. Phased plan
|
||||||
|
|
||||||
|
- **P0 — this document.** Review in the 2026-08-24 meeting, merge.
|
||||||
|
- **P1 — tracer bullet (1–2 days, demo-able).** No product code: a scratch
|
||||||
|
compose file on a **clean** test VPS + a test domain with wildcard A record
|
||||||
|
+ a Furtka VM from the current ISO. Manual WG configs both ends, static
|
||||||
|
Caddyfile with On-Demand TLS and hardcoded routes to jellyfin/HA. Validates
|
||||||
|
the three real risks before any pairing code exists: on-demand LE issuance
|
||||||
|
under BYO domain, WebSockets through the whole chain, tunnel latency/MTU.
|
||||||
|
Demo: `https://jellyfin.<testdomain>` on a phone on LTE.
|
||||||
|
- **P2 — gatewayd v1** (furtka-gateway repo): pairing, routes PUT, tenant
|
||||||
|
state, WG peer mgmt, Caddyfile render + admin-API push, `ask`, gatewayctl,
|
||||||
|
install.sh, tests, release pipeline. Validation: pair via `curl` by hand
|
||||||
|
from the P1 VM; kill/restart containers, confirm state re-render.
|
||||||
|
- **P3 — box client** (core): `furtka/remote.py`, CLI verbs, API handlers,
|
||||||
|
wg-quick unit, nftables rules, ISO packages. Validation: fresh ISO install
|
||||||
|
→ `furtka remote pair example.de K7PF-…` → tunnel up, `furtka remote
|
||||||
|
status` green, port scan **from the VPS** confirms the firewall.
|
||||||
|
- **P4 — `expose` + route sync** (furtka-apps + core): `expose` for
|
||||||
|
jellyfin, home-assistant, uptime-kuma, it-tools (`auth:none`); manifest
|
||||||
|
parsing; sync hooks on install/uninstall/toggle; server-side `open_url`
|
||||||
|
rewrite. Validation: toggle → live in <5 s; uninstall → route gone; old
|
||||||
|
core parses new catalog (backward-compat test).
|
||||||
|
- **P5 — UI + docs** (core): settings card, pairing form, per-app toggles +
|
||||||
|
warning modal, user guide, wizard-flow.md revision, website/README.
|
||||||
|
Validation: the full dad test — clean ISO → pair → toggle → phone on LTE,
|
||||||
|
without ever opening a terminal on the box.
|
||||||
|
- **P6 — later, unscoped**: Authentik + `forward` auth, managed multi-tenant
|
||||||
|
operation, TCP passthrough (MQTT), multi-box, family WG profiles.
|
||||||
|
|
||||||
|
## 12. Open questions (decide with Robert)
|
||||||
|
|
||||||
|
1. **Reserved name**: `gw.<domain>` vs `furtka-gw.<domain>` — and whether
|
||||||
|
the admin UI (`furtka.<domain>`) should *ever* be exposable.
|
||||||
|
2. **Gateway Basic Auth**: in v1 or cut to shrink scope? (Lean: **in** —
|
||||||
|
~20 lines, and it's the only guardrail for it-tools-class apps.)
|
||||||
|
3. **Install UX**: `curl | bash` one-liner vs documented 3-step install.
|
||||||
|
4. **German UI wording** ("Fernzugriff"? "Sicherer Zugang von unterwegs"?)
|
||||||
|
and wizard scope: redesign S5/S6 now, or ship settings-page-first and
|
||||||
|
revise the (still unimplemented) wizard afterwards? (Lean:
|
||||||
|
settings-first.)
|
||||||
Loading…
Add table
Reference in a new issue