fix(webui): require token_issue_secret for LAN access with frontend auth

When host is set to 0.0.0.0, the gateway now enforces that either token
or token_issue_secret must be configured — it refuses to start otherwise.

Bootstrap endpoint behavior:
- token_issue_secret configured: always validate regardless of source IP
  (handles reverse-proxy scenarios where all connections appear as localhost)
- No secret: only localhost can bootstrap (local dev mode)

The frontend shows an authentication form when bootstrap returns 401/403,
persists the secret in localStorage, and retries automatically on reload.
This commit is contained in:
chengyongru
2026-05-06 23:51:51 +08:00
committed by Xubin Ren
parent 034bea1a44
commit 4efd904ccc
8 changed files with 265 additions and 49 deletions
+15 -4
View File
@@ -128,6 +128,17 @@ class WebSocketConfig(Base):
raise ValueError("token_issue_path must differ from path (the WebSocket upgrade path)")
return self
@model_validator(mode="after")
def wildcard_host_requires_auth(self) -> Self:
if self.host not in ("0.0.0.0", "::"):
return self
if self.token.strip() or self.token_issue_secret.strip():
return self
raise ValueError(
"host is 0.0.0.0 (all interfaces) but neither token nor "
"token_issue_secret is set — set one to prevent unauthenticated access"
)
def _http_json_response(data: dict[str, Any], *, status: int = 200) -> Response:
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
@@ -607,10 +618,10 @@ class WebSocketChannel(BaseChannel):
self._api_tokens.pop(token_key, None)
def _handle_webui_bootstrap(self, connection: Any, request: Any) -> Response:
# When token_issue_secret is configured, validate it regardless of
# source IP. This secures deployments behind a reverse proxy (e.g.
# nginx) where all connections appear as localhost.
secret = self.config.token_issue_secret.strip()
# When a secret is configured (token_issue_secret or static token),
# validate it regardless of source IP. This secures deployments
# behind a reverse proxy where all connections appear as localhost.
secret = self.config.token_issue_secret.strip() or self.config.token.strip()
if secret:
if not _issue_route_secret_matches(request.headers, secret):
return _http_error(401, "Unauthorized")