Close unauthenticated remote mutation paths and keep filesystem destinations behind one-shot Tauri capabilities. CodeRabbit and Greptile findings were fixed on-branch; all review threads are resolved. Full CI, Security, Rust, and cross-platform smoke checks are green.
CodeRabbit: the 6-digit share PIN is brute-forceable (10^6, no lockout), so
letting it unlock admin over the network was still weak. Admin now requires the
API key (a long operator secret) or loopback; the PIN is consumption-only and
never gates /system/* or /api/settings/*. A PIN-only deployment keeps admin
loopback-only. Docs (api-auth.md, remote-gpu.md) aligned with the conditional
gate (no credential -> admin open; API key -> admin) and the PIN exclusion.
Test inverted: presenting the PIN over the network is now 403 on admin.
OMNIVOICE_SERVER_MODE=1 made require_loopback an unconditional no-op, so with
OMNIVOICE_TRUSTED_NETWORKS also set a trusted-CIDR client — a consumption-only
exemption that bypasses the PIN/API-key middleware via is_local_host — could
reach the RCE-class admin surface (/system/set-env, /api/settings/*) with no
credential. That collapsed the documented two-tier privilege model
(consumption trust != admin trust) in exactly the "lock the backend with a key,
exempt a LAN proxy for TTS" configuration.
Server mode still can't require true loopback (Docker NAT, #261), but it now
applies the credential rule to admin routes: open only when NO credential is
configured; otherwise the request must present the API key or share PIN.
Trusted-network membership alone never satisfies it. Loopback, credential
holders, and no-credential Docker deployments are unchanged; consumption
routes (require_local / middleware) keep exempting trusted networks.
Regression tests cover the server-mode x trusted-network x credential matrix
(fail-before/pass-after). Docs: new docs/api-auth.md two-tier model + quick
reference; docs/remote-gpu.md corrected (previously documented the hole as
accepted behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-hosting behind a reverse proxy or on a LAN used to force a blunt choice:
OMNIVOICE_SERVER_MODE (trust every non-loopback source) or the API-key/PIN gates
— which a proxy that strips the Authorization header breaks for browser clients
entirely.
Add OMNIVOICE_TRUSTED_NETWORKS (comma-separated CIDRs) whose addresses are
treated as trusted by the CONSUMPTION gates (PIN/API-key middleware, dictation
WebSocket) via is_local_host — a LAN/proxy client is exempted from consumption
auth. Admin routes (require_loopback → /system/set-env, /api/settings/*) stay
true-loopback-only (is_loopback, not is_local_host) to preserve the two-tier
privilege model: consumption trust ≠ admin trust (RCE-class surface). Opt-in,
default empty → zero behavior change. The granular companion to server-mode (#261).
Tests: is_loopback / is_local_host / require_loopback contract for trusted CIDRs,
adjacent subnets, malformed entries, the two-tier split (trusted-network rejected
by the admin gate), and the default (no-trust) case. Docs + CHANGELOG.
In Docker the loopback origin gate (`require_loopback`) is unenforceable:
Docker's NAT rewrites `request.client.host` to the bridge gateway (e.g.
172.17.0.1) even for a localhost-only `-p 127.0.0.1:3900:3900` mapping, so every
request looks non-loopback. The gate then 403s the operator out of the routes
the web UI needs — `/system/*` (incl. `/system/info`, which left the version
blank, re-breaking #249 in Docker) and `/api/settings/*` (HF-token entry) —
surfacing as "Loopback origin required" all over the UI.
Fix: add an explicit, opt-in `OMNIVOICE_SERVER_MODE` flag. When set,
`require_loopback` becomes a no-op; exposure is then governed by the operator's
port mapping plus the optional share PIN (NetworkAccessMiddleware still 401s
unauthenticated non-loopback clients whenever a PIN is set). The Docker image
sets `OMNIVOICE_SERVER_MODE=1` (Dockerfile + documented in compose).
Security: the desktop build NEVER sets this, so its loopback boundary is
unchanged — LAN share guests are still denied the admin/system routes. New
unit tests lock the contract (strict 403 by default incl. the PR #81 vectors;
relaxed only under the flag). Existing non-loopback 403 tests still pass.
Docs: docker.md troubleshooting entry for "Loopback origin required".
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>