fix: onboarding unblocked for reverse proxy / SSH tunnel deployments (fixes #390) (#391)

- Read X-Forwarded-For and X-Real-IP before falling back to raw socket IP
- Add HERMES_WEBUI_ONBOARDING_OPEN=1 env var escape hatch for remote servers
- Error message now includes the env var hint
- 18 new tests (TestOnboardingIPLogic + TestOnboardingSetupEndpoint)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-13 17:52:07 -07:00
committed by GitHub
parent acc14f2f0b
commit 2acee7fc34
4 changed files with 205 additions and 4 deletions

View File

@@ -914,16 +914,26 @@ def handle_post(handler, parsed) -> bool:
# Writing API keys to disk - restrict to local/private networks unless auth is active.
# In Docker, requests arrive from the bridge network (172.x.x.x), not 127.0.0.1,
# even when the user accesses via localhost:8787 on the host.
# Behind a reverse proxy (nginx/Caddy/Traefik) or SSH tunnel, X-Forwarded-For
# carries the real origin IP — read it first before falling back to the raw socket addr.
# HERMES_WEBUI_ONBOARDING_OPEN=1 lets operators on remote servers explicitly bypass
# the check when they control network access themselves (e.g. firewall + VPN).
from api.auth import is_auth_enabled
if not is_auth_enabled():
import os as _os
if not is_auth_enabled() and not _os.getenv("HERMES_WEBUI_ONBOARDING_OPEN"):
import ipaddress
try:
addr = ipaddress.ip_address(handler.client_address[0])
# Prefer forwarded headers set by reverse proxies
_xff = handler.headers.get("X-Forwarded-For", "").split(",")[0].strip()
_xri = handler.headers.get("X-Real-IP", "").strip()
_raw = handler.client_address[0]
_ip_str = _xff or _xri or _raw
addr = ipaddress.ip_address(_ip_str)
is_local = addr.is_loopback or addr.is_private
except ValueError:
is_local = False
if not is_local:
return bad(handler, "Onboarding setup is only available from local networks when auth is not enabled.", 403)
return bad(handler, "Onboarding setup is only available from local networks when auth is not enabled. To bypass this on a remote server, set HERMES_WEBUI_ONBOARDING_OPEN=1.", 403)
try:
return j(handler, apply_onboarding_setup(body))
except ValueError as e: