Files
serverAppConfigs/serverAppConfigs-docs/Dankclaw/Hermes-WebUI.md
T
Frank 2e179b024d docs(hermes): document namespace addressability and fix stale claims
New page Architecture/Container Network Namespaces.md: which local services are
addressable from which Podman network namespace on Dankclaw (rootless podman +
netavark). Verified live 2026-09-02: gateway/webui/CDP run in the host netns and
dial loopback; hermes-dashboard is the only Hermes container on a bridge network
(dankclaw_default, 10.89.1.0/24 gw 10.89.1.1) and can dial published ports only —
unpublished host-netns listeners (signal-cli :8080, webui :8787) are refused from
that namespace even though both bind 0.0.0.0.

Corrected while verifying the above:
- Hermes.md: data root is /home/dank/Containers/hermes/data (not the retired
  /home/dank/hermes-data); gateway dials the Signal daemon on loopback; the
  daemon itself is a host user unit (signal-cli-daemon.service) bound to
  0.0.0.0:8080, so the Signal HTTP API is LAN-reachable.
- Hermes-Dashboard.md: it is a Quadlet unit (Network=dankclaw_default), not a
  compose service; data root + /opt/data anonymous volume corrected.
- Hermes-WebUI.md: binds 0.0.0.0:8787 in the host netns (verified: unauthenticated
  GET /health from another LAN machine returned 200) — the SSH tunnel is an access
  path, not a containment boundary; HealthCmd and data-path corrections.
- Network Architecture.md: no hermes vhost exists in either the tracked or the live
  Caddyfile (hermes.franksdank.cloud resolves but routes nowhere); SearXNG exits the
  tunnel via the shared named network searxng.network, not service:gluetun.

Docs only — deploy is a no-op for this change.
2026-09-02 16:10:57 -07:00

144 lines
7.2 KiB
Markdown

---
type: configuration
title: Hermes-WebUI (Hermex server)
description: The hermes-webui container on Dankclaw — the API server that the Hermex iPhone app talks to, reachable over an SSH tunnel and, because it binds all interfaces, from the whole LAN.
tags:
- hermes
- hermes-webui
- hermex
- dankclaw
- ssh-tunnel
timestamp: 2026-09-14
---
# Hermes-WebUI (Hermex server)
`hermes-webui` is the [hermes-webui](https://github.com/nesquena/hermes-webui)
server (MIT, third-party) that the **Hermex** iPhone app connects to. It runs
the Hermes agent in-process from the shared `HERMES_HOME`, so it is the *same*
agent — same memory, skills, and sessions — as the [[Hermes]] gateway and the
[[Hermes-Dashboard]].
**Host:** Dankclaw — **Port:** `0.0.0.0:8787` (all interfaces) — **Ingress:** SSH tunnel for the phone app; direct LAN access also works
## Facts
| Item | Value |
|------|-------|
| Image | `docker.io/nousresearch/hermes-agent:latest` (same as the gateway) |
| Command | `python3 /webui/bootstrap.py --foreground` |
| Source checkout | `/home/dank/Containers/hermes/webui` on Dankclaw, bind-mounted at `/webui` |
| Port | `8787`, bound to `0.0.0.0` in the **host** network namespace (`Network=host`) — LAN-reachable, not loopback-only |
| Auth | `HERMES_WEBUI_PASSWORD` (host `.env`; Hermex signs in with it) |
| Data | bind mount `/home/dank/Containers/hermes/data` → `/data`, shared with the gateway and dashboard; anonymous volume at `/opt/data` |
| Health | `HealthCmd=` in the Quadlet unit probes `http://127.0.0.1:8787/health` from inside the unit (works because the webui runs in the host netns); the deployer probes the same path from the host |
## Why a container, not a host process
The gateway's state files are owned by the container-mapped uid (`109999`,
from rootless Podman's `1 → 100000/65536` subuid range). A fresh rootless
namespace on Dankclaw maps identically (verified with `podman unshare
cat /proc/self/uid_map`), so a second container using the same image reads
and writes the shared volume with exactly the ownership the gateway already
uses. A host-side process running as `dank` (uid 1000) could not read the
gateway's `0600` state files.
## How it works
- `bootstrap.py` creates `/webui/.venv` on first boot, pip-installs the WebUI
and agent requirements, then **execs** `server.py` as one foreground process
(`--foreground`). systemd-free: Podman supervises it via
`restart: unless-stopped`.
- `HERMES_WEBUI_SKIP_ONBOARDING=1` keeps the first-run wizard off; a profile
already exists in `/data` from the gateway.
- The healthcheck `start_period` is 600 s because the very first boot spends
minutes pip-installing into a fresh venv.
## Remote access (Hermex on iOS — persistent SSH tunnel)
Only port `22` is forwarded to Dankclaw at the router, so the phone reaches the
webui through an SSH **local forward** — no extra router ports, no third-party
relay (no Tailscale, no Cloudflare). Implemented 2026-08-31. The tunnel is an
*access path* for the phone, **not** a containment boundary: the server binds
`0.0.0.0:8787`, so any host that can route to `192.168.0.143` can reach the API
directly and needs only `HERMES_WEBUI_PASSWORD` to get in (verified 2026-09-02:
an unauthenticated `GET /health` from another LAN machine returned HTTP 200).
See [[Container Network Namespaces]] for why the webui is reachable this way and
the sibling dashboard on `9119` is not.
- Dedicated `tunnel` account on Dankclaw (`/usr/sbin/nologin` shell, password
locked). Authentication is key-only; the account can do nothing except
authenticate — `ForceCommand /usr/sbin/nologin` kills any session channel
while the transport stays up for port forwarding.
- sshd drop-in `/etc/ssh/sshd_config.d/99-tunnel.conf` scopes
`AllowTcpForwarding yes` to that user only (plus `PermitTTY no`,
`X11Forwarding no`).
- Key options on the authorized key pin the blast radius:
`command="/usr/sbin/nologin",no-pty,restrict,port-forwarding,permitopen="localhost:8787"`
— a leaked tunnel key can only forward to `localhost:8787`, never act as a
general SOCKS/SSH proxy.
- On the phone (client side): create a key on-device (private key never
leaves the device), hand the **public** key to the admin who appends it to
`/home/tunnel/.ssh/authorized_keys`, then connect from the SSH client:
- In-network target `192.168.0.143:22`, off-network target the public
hostname (router forwards only port 22 → Dankclaw) — same forward config
otherwise.
- Local forward `127.0.0.1:8787 → localhost:8787`; Hermex server URL is then
`http://localhost:8787` in both locations.
- For a *persistent* tunnel on iOS, Secure ShellFish's personal-VPN mode
(Network Extension keeps the tunnel up and routes `127.0.0.0/8` through
it); Termius works but only while its session is connected — iOS allows no
background SSH daemon.
```mermaid
graph LR
A[Hermex app] -->|http://localhost:8787| B[SSH local forward on iPhone<br/>persistent via personal VPN]
B ==>|encrypted SSH to port 22| C[Dankclaw sshd<br/>tunnel user, forced nologin]
C --> D["hermes-webui 0.0.0.0:8787 (host netns)"]
D --> E["/home/dank/Containers/hermes/data (shared agent state)"]
```
## Deployment
```
./scripts/deploy.sh dankclaw --service hermes-webui
```
`deploy.sh` copies `dankclaw/hermes/quadlet/*.container` into the quadlet unit
directory and restarts `hermes.service`, `hermes-dashboard.service` and
`hermes-webui.service` in the **user** manager of `dank` (linger must be on for
them to come up at boot), so gateway/dashboard/webui restarts stay isolated from
inference deploys.
Before first deploy, set a strong `HERMES_WEBUI_PASSWORD` in the live
`dankclaw/.env`.
## Gotchas
- The `/webui` mount is **rw on purpose**: `bootstrap.py` creates `.venv` in
the repo root. Do not flip it to `:ro`.
- The webui binds `0.0.0.0:8787` today, so the SSH tunnel is an access path for
the phone, not a security boundary: any LAN host that can route to
`192.168.0.143` reaches the API and needs only `HERMES_WEBUI_PASSWORD`. The
webui can run agent commands, so treat that password as a credential with
command-execution blast radius. To make loopback-only binding true again, set
`HERMES_WEBUI_HOST=127.0.0.1` in the unit and require the tunnel for every
client — that is an exposure change to decide deliberately, not a silent fix.
- The webui and the gateway share one data root (`/data` is a bind mount of
`/home/dank/Containers/hermes/data`). A config change made on one side takes
effect when the other restarts (same caveat as the dashboard).
- `bootstrap.py` auto-installs a Hermes Agent if it cannot find one; in this
image the agent is already present, so that path should never fire. If the
log shows an install attempt, the image changed — re-verify the entrypoint.
# Citations
- hermes-webui README (env vars, `--foreground`, `/health`): https://github.com/nesquena/hermes-webui
- Hermex setup model (server URL + password; Cloudflare/Tailscale are optional, not required): https://hermexapp.com/
## See Also
- [[Hermes]] — the agent and its Signal gateway
- [[Hermes-Dashboard]] — the LAN browser management UI
- [[Dankclaw Stack]]
- [[Container Network Namespaces]] — why this host-netns service is LAN-reachable