Skip to content

Dashboard Security Watchdog

The Hermes dashboard on the Agent Host VPS is exposed through a Cloudflare Tunnel (hermes.ducatillon.net → https://localhost:9119). Binding the dashboard to the loopback address (127.0.0.1) is a security control, but it introduces a Host-header validation issue: the tunnel forwards requests with Host: hermes.ducatillon.net, which Hermes’s _is_accepted_host() rejects by default with:

{"detail":"Invalid Host header. Dashboard requests must use the hostname the server was bound to."}

This page documents the patch that solves it and the watchdog that keeps that patch applied across upgrades.

Hermes’s dashboard backend (hermes_cli/web_server.py) normally accepts only loopback hostnames (localhost, 127.0.0.1, ::1). A local patch adds an _trusted_loopback_hosts() function that folds in the tunnel’s public hostname (passed via the HERMES_DASHBOARD_TRUSTED_HOSTS environment variable), wired into the three validation points:

  1. the _trusted_loopback_hosts() helper itself (after _LOOPBACK_HOST_VALUES),
  2. _is_accepted_host() — accepts hermes.ducatillon.net as a trusted host,
  3. should_require_auth() — treats the trusted host as loopback (Cloudflare Access OTP still gates all traffic at the edge, so this does not reduce security).

The dashboard service file sets the env var and binds to 127.0.0.1 only (source: agent-host-ops skill — ~/.hermes/skills/devops/agent-host-ops on the Agent Host).

hermes update pulls the latest hermes-agent source, which overwrites web_server.py and silently removes the patch. The dashboard keeps running, but every request routed through the tunnel is rejected with the Invalid Host header error. This is the only file the update overwrites in the estate, so the patch must be restored after every upgrade.

The dashboard rejects tunnel requests until the watchdog re-applies the patch on its next tick. The watchdog runs on the cron expression */30 * * * * (fires at :00/:30 UTC), so the repair happens within ≤30 min of an upgrade, with exactly one Slack alert. The local service on 127.0.0.1:9119 keeps running throughout and still answers direct requests.

No action is required. For an immediate restore, run the script manually (below).

ensure_dashboard_security.py is an idempotent, manifest-free repair script. It:

  1. checks whether _trusted_loopback_hosts() is present in web_server.py,
  2. re-applies all three patch points if missing,
  3. ensures the dashboard service binds to 127.0.0.1 with HERMES_DASHBOARD_TRUSTED_HOSTS set,
  4. restarts the dashboard only if a change was actually made.

Running it repeatedly is safe — when everything is already in place, it exits with All security settings already in place (no changes needed).

Location: ~/.hermes/scripts/ensure_dashboard_security.py

Verify after running:

Terminal window
# Simulate the tunnel Host header — must return 200, not 400:
curl -s -o /dev/null -w "%{http_code}" -H "Host: hermes.ducatillon.net" http://127.0.0.1:9119/

ensure_dashboard_security_watchdog.py wraps the repair script in the watchdog pattern: it checks the patch marker itself first, and only invokes the repair script when the patch is actually missing.

  • Healthy → empty stdout → cron stays silent (no message delivered).
  • Repaired → prints what it did → cron delivers exactly one alert.

Job details:

Field Value
Schedule */30 * * * * (cron — fires :00/:30 UTC)
Mode no_agent: true (script only, no LLM)
Script ensure_dashboard_security_watchdog.py (~/.hermes/scripts/)
Delivery Slack (security channel) — only when a repair happened
Terminal window
python3 ~/.hermes/scripts/ensure_dashboard_security.py

Then reload the page and re-authenticate with the Cloudflare one-time passcode.