fix(docker): let Provider Settings admit the host through the published port

Docker delivers the host's own browser to the container from the container
network's gateway, never from 127.0.0.1, so the loopback-only admission gate
answered every POWER UP request with 403 and no key could be saved. The README
and CHANGELOG claimed otherwise; both are corrected here.

- keySetupCore: GEV_KEY_SETUP_TRUSTED_PEERS, an exact-address list (no
  wildcards, prefixes or hostnames) admitted alongside loopback. Unset trusts
  nothing, and the sharing, Host, Origin, Content-Type and proxy-header checks
  all still apply to a listed address. Twelve assertions pin each refusal.
- scripts/docker-start.mjs: the container entry point, shaped after
  scripts/pinokio-start.mjs. It reads the default gateway from /proc/net/route,
  trusts exactly that address, and starts Vite. No gateway (--network none)
  trusts nothing. The pure helpers are unit-tested and a contract test pins the
  Dockerfile CMD and the loopback-only publish.
- Dockerfile: install git. `npm test` builds fixtures with `git init`, so the
  suite failed 71 tests inside the image and the README's claim that it runs in
  the container was wrong.
- SECURITY.md and .env.example document the setting and why it is safe only
  behind a 127.0.0.1 publish; README gains the Linux/NAS id note and .dockerignore
  drops macOS AppleDouble sidecars, both reported on the PR.

Verified in Docker on a clean build: saving a key from the panel writes the
bind-mounted .env (0600 in the container), it survives `compose down`/`up`, and
REMOVE comments it back out. A foreign Origin, a foreign Host, a proxy header
and a sibling container on the same network are each refused. `npm run doctor`,
`npm test` (5067 passed, 1 skipped, 0 failed) and `npm run build` are green
inside the container.

Reported-by: @jklaze
Co-Authored-By: Bogdan Petrut <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
committed 2026-10-10 19:48:02 +02:00
1 parent 2e50836b08
commit 1c6c291774
10 files changed
+328 -9

No files matched your search

+11
View File
@@ -84,6 +84,17 @@ The dev server is a **key broker**: every server-side key above is spendable by
- **Local-only by default.** `./scripts/dev-fresh.sh` (and the Vite config itself) bind to `localhost`, so only your machine can reach the server — and only local names are accepted (`allowedHosts` stays restricted, which also blunts DNS-rebinding tricks). Binding to all interfaces keeps that restriction: IP addresses and `localhost` work, and LAN hostnames must be listed in `GEV_ALLOWED_HOSTS` (for example `GEV_ALLOWED_HOSTS=globe.lan`); suffix and wildcard entries are ignored.
- **LAN exposure is an explicit opt-in**: `HOST=0.0.0.0 ./scripts/dev-fresh.sh`. The launcher prints a prominent warning plus your LAN URL. Understand what opting in means: **every device on that network can drive the proxies and spend your OpenAI / Google / OpenSky / AISStream / TomTom / FIRMS quota** for as long as the server runs. Do this only on networks you trust.
- **App-level throttles (on by default):** `GEV_RATELIMIT_OPENAI_PER_MIN` (default `30`) and `GEV_RATELIMIT_GOOGLE_PER_MIN` (default `120`) — the values the Pinokio build already ships — cap the cost-bearing endpoints per client IP per minute (over-limit requests receive a sanitized `429`). Both defaults sit well above what the app itself generates; set either to exactly `0` to run unthrottled. They are **per-IP, process-local, in-memory guards** — they reset on restart, a caller with several addresses gets a bucket per address, and they are **not billing caps**.
- **Docker trusts one address, and only because the port is on loopback.**
Docker delivers the host's own browser to the container from the container
network's gateway, never from `127.0.0.1`, so a loopback-only Provider
Settings refuses the machine running it. `scripts/docker-start.mjs` therefore
sets `GEV_KEY_SETUP_TRUSTED_PEERS` to exactly that gateway address. The list
takes exact addresses only (no wildcards, prefixes or hostnames), is unset
under every other launcher, and relaxes none of the sharing, Host, Origin,
Content-Type or proxy-header checks. It is safe precisely because
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
client arrives from the same gateway address, which hands them Provider
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
Pinokio release re-reads sharing state when an app registers its Open URL and