Multi-endpoint pairing (ADR 24): - QR schema now carries an optional ordered `endpoints` array with strict priority + reachability-as-tiebreaker semantics. Emits `hermes:3` when endpoints are present; `hermes:2` stays when only a legacy single-URL pair is emitted. Old v1/v2 QRs parse unchanged — phone synthesizes a priority-0 candidate for them. - Reachability probe: HEAD /health per candidate, 2s timeout, 30s per-endpoint cache. NetworkCallback re-probes on network change. - Paired Devices screen now renders one row per (device, endpoint); Settings gains an Endpoints card with per-endpoint health chip + manual override + "probe now" + SPKI pin inspection. - Open-vocabulary roles: `lan` / `tailscale` / `public` get styled chips; unknown roles render as "Custom VPN (<role>)". HMAC canonicalization preserves role strings verbatim + array order. First-class Tailscale (ADR 25): - New `plugin/relay/tailscale.py` + `tailscale_cli.py` helpers + `scripts/hermes-relay-tailscale` shell shim (mirrors `hermes-pair`). Shells `tailscale serve --bg --https=<port> http://127.0.0.1:<port>`. - `install.sh` gains optional step 7/7 — silent skip when binary absent or TS_DECLINE=1; auto-enable with TS_AUTO=1. - `pair.py --mode auto` auto-detects Tailscale and emits a priority-1 `tailscale` endpoint when available. - Auto-retires when upstream hermes-agent PR #9295 merges, via `canonical_upstream_present()` probe — same shape as `hermes_relay_bootstrap/`. Dashboard Remote Access tab: - New 5th tab surfacing Tailscale status, public URL pin, live reachability, and one-click QR regen with current modes. - 6 new loopback-only proxy routes under `/api/plugins/hermes-relay/remote-access/*`. - Bundle rebuilt (plugin/dashboard/dist/index.js). Docs: - New `docs/remote-access.md` operator-facing setup guide covering Tailscale (recommended) / Caddy+LE / Cloudflare Tunnel / WireGuard / plaintext-over-VPN with working config blocks. - security.md + relay-server.md + spec.md §3.3 + README.md + CHANGELOG.md + DEVLOG.md + CLAUDE.md all synced. Tests: - Python: 71 new/updated tests (pairing schema + QR sign canonical form + Tailscale helper + dashboard proxy routes). - Android: 9 HermesPairingPayload tests (v1/v2/v3 + role vocab) + 7 MockWebServer-backed EndpointResolver tests. - Lint + compileGooglePlayDebugKotlin green locally. Restored `profilesUpdatedEvents` emitter in AuthManager / ConnectionViewModel that the Kt-Payload agent had collaterally deleted while adding endpoint persistence — unrelated but the bundle would not compile without it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
Security
Overview
Hermes-Relay gives a remote AI agent full control of an Android device via AccessibilityService. This is powerful and inherently sensitive — treat it with the same caution as remote desktop access.
Current Security Model
Authentication
- Pairing code: A random 6-character alphanumeric code. For the QR-driven flow, the pair command (
/hermes-relay-pairskill orhermes-pairshell shim) generates the code on the Hermes host and pre-registers it with the relay via the loopback-onlyPOST /pairing/registerendpoint; the phone-sideAuthManager.generatePairingCode()generator is retained for the Phase 3 bridge flow. - The phone and server must share this code to establish a connection.
- Codes use the full
A-Z / 0-9alphabet (36 chars). The earlier "no ambiguous 0/O/1/I" restriction was dropped when the pairing flow moved from "human retypes code from display" to "code flows phone ↔ server via QR + HTTP" (seedocs/decisions.md§6a). POST /pairing/registeris gated to loopback callers only (127.0.0.1/::1) — only a process with host shell access on the relay machine can inject pairing codes. A LAN attacker cannot.
Rate Limiting
- Failed WebSocket authentication attempts are rate-limited per IP
- After 5 failed attempts in 60 seconds, the IP is blocked for 5 minutes
- Blocked IPs receive HTTP 429
Connection Architecture
- The phone connects out to the server (NAT-friendly)
- The server relay only accepts one phone at a time
- All tool commands are proxied through the relay — the phone is never directly exposed
Known Limitations (Prototype)
No Encryption
WebSocket connections use ws:// (plaintext), not wss:// (TLS). This means:
- Commands, screen content, and screenshots travel unencrypted
- Anyone on the network path between phone and server can intercept traffic
- Mitigation: Use over a trusted network, or set up a reverse proxy with TLS (nginx/caddy)
Full Device Access
Once paired, the agent has unrestricted access to:
- Read all screen content (any app)
- Tap, type, swipe anywhere
- Open any app
- Take screenshots
- Read installed app list
There is no granular permission system — the agent can access banking apps, messages, etc.
- Mitigation: Only pair with trusted Hermes instances. Disconnect when not in use.
No Command Audit Log
There is no persistent log of what commands the agent executed on the phone.
- Mitigation: The relay logs commands to stdout when run with INFO logging.
Remote connectivity
Hermes-Relay does not ship its own application-layer crypto. The operator owns both endpoints, and the trust model assumes TLS is terminated somewhere on the path that the operator already controls — Tailscale (managed TLS + tailnet ACL identity), a reverse proxy with Let's Encrypt, a WireGuard / other VPN, or a Cloudflare Tunnel. See docs/remote-access.md for the decision matrix and setup recipes per mode.
Multi-endpoint pairing (ADR 24) makes "same phone, different networks" a first-class case: a single QR carries lan / tailscale / public candidates in strict-priority order and the phone re-probes reachability on every network change. Per-candidate transport_hint drives the plaintext-ws:// consent dialog — explicit operator consent is still required for any unencrypted leg. The TOFU cert pin is keyed by host:port, so two endpoints pointing at the same hostname share a pin (correct — same cert, same pin) while distinct hostnames each get their own.
Recommendations for Production Use
- Use Tailscale (built-in) or a reverse proxy + Let's Encrypt.
hermes-relay-tailscale enableis the shortest path to a managed-TLS remote-access story; Caddy + Let's Encrypt is the shortest path to a real public domain. Either works. Seedocs/remote-access.md. - Use a strong pairing code: Don't share your code publicly
- Disconnect when idle: Tap Disconnect in the app when you're not actively using it
- Monitor the phone: Keep the status overlay enabled to see when the bridge is active
- Don't pair on public WiFi without a VPN: Plain
ws://is vulnerable to interception — pair on a network you trust or front the relay with Tailscale / TLS first.
Privacy
See docs/privacy.md for the full privacy and data handling policy. Key points:
- No external data transmission — the app connects only to your self-hosted Hermes servers
- Local-only analytics — Stats for Nerds counters are stored in DataStore on-device, never sent externally
- Encrypted credential storage — API keys and session tokens use EncryptedSharedPreferences (AES-256-GCM, hardware-backed Android Keystore)
- No tracking, ads, or third-party SDKs
Reporting Security Issues
If you find a security vulnerability, please open an issue on GitHub or contact the maintainers directly. Do not exploit vulnerabilities on other people's devices.