Server-side implementation landed on Dankclaw: dedicated 'tunnel' account (nologin shell, password-locked, key-only auth), sshd drop-in 99-tunnel.conf scoping AllowTcpForwarding to that user with PermitTTY off, and permitopen="localhost:8787" key-option guidance so a leaked tunnel key can only forward to the webui port. WebUI stays loopback-only published.
128 lines
5.6 KiB
Markdown
128 lines
5.6 KiB
Markdown
---
|
|
type: configuration
|
|
title: Hermes-WebUI (Hermex server)
|
|
description: The hermes-webui compose service on Dankclaw — the loopback-only API server that the Hermex iPhone app talks to over an SSH tunnel.
|
|
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:** `127.0.0.1:8787` (loopback only) — **Ingress:** SSH tunnel
|
|
|
|
## Facts
|
|
|
|
| Item | Value |
|
|
|------|-------|
|
|
| Image | `docker.io/nousresearch/hermes-agent:latest` (same as the gateway) |
|
|
| Command | `python3 /webui/bootstrap.py --foreground` |
|
|
| Source checkout | `/home/dank/hermes-webui` on Dankclaw (mounted at `/webui`) |
|
|
| Port | `8787`, published to `127.0.0.1` only |
|
|
| Auth | `HERMES_WEBUI_PASSWORD` (host `.env`; Hermex signs in with it) |
|
|
| Data | shares `/home/dank/hermes-data` with the gateway and dashboard |
|
|
| Health | `GET /health` (compose healthcheck + deployer) |
|
|
|
|
## 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 webui is
|
|
reached through an SSH **local forward** — no extra router ports, no
|
|
third-party relay (no Tailscale, no Cloudflare). Implemented 2026-08-31:
|
|
|
|
- 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 127.0.0.1:8787"]
|
|
D --> E["/home/dank/hermes-data (shared agent state)"]
|
|
```
|
|
|
|
## Deployment
|
|
|
|
```
|
|
./scripts/deploy.sh dankclaw --service hermes-webui
|
|
```
|
|
|
|
The planner maps changes to the `hermes-webui` compose block (and only that
|
|
block) to this service, so gateway and inference restarts stay isolated.
|
|
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`.
|
|
- Never publish `8787` beyond loopback without keeping the password set —
|
|
the webui can run agent commands.
|
|
- The webui and the gateway share one data volume. 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]]
|