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.
The patch
Section titled “The patch”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:
- the
_trusted_loopback_hosts()helper itself (after_LOOPBACK_HOST_VALUES), _is_accepted_host()— acceptshermes.ducatillon.netas a trusted host,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).
Why the patch disappears
Section titled “Why the patch disappears”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.
What to expect right after an update
Section titled “What to expect right after an update”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).
The script
Section titled “The script”ensure_dashboard_security.py is an idempotent, manifest-free repair script. It:
- checks whether
_trusted_loopback_hosts()is present inweb_server.py, - re-applies all three patch points if missing,
- ensures the dashboard service binds to
127.0.0.1withHERMES_DASHBOARD_TRUSTED_HOSTSset, - 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:
# 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/The watchdog
Section titled “The watchdog”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 |
Restoring manually
Section titled “Restoring manually”python3 ~/.hermes/scripts/ensure_dashboard_security.pyThen reload the page and re-authenticate with the Cloudflare one-time passcode.