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>