# Runtime and security What the relay exposes at runtime, what is encrypted end to end, what a transport intermediary can still observe, and what the audit log records. Read this before exposing a relay beyond your own machine. ## Ports and exposed surface The relay binds to `127.0.0.1:8375` by default (`HERDR_RELAY_HOST`, `HERDR_RELAY_PORT`); its event hook uses loopback UDP 8376 (`HERDR_RELAY_PLUGIN_PORT`). A Cloudflare tunnel supplies HTTPS/WSS without opening an inbound port. On the gateway transport, an outbound connection to the gateway replaces the tunnel, and the direct WebRTC path adds one UDP socket, guarded by ICE credentials and a pinned DTLS fingerprint exchanged only inside the encrypted channel; see [transports.md](transports.md). Tokens use constant-time comparison, uploads are limited, and launch requests cannot provide arbitrary executables or shell commands. Workspace and worktree mutations use Herdr's typed API; the phone creates checkouts under Herdr's configured worktree directory and can open only paths returned by Herdr's worktree list. A keyed connection is authorized by the end-to-end handshake rather than by its browser origin; the `HERDR_ALLOWED_ORIGINS` check applies to tokenless loopback development connections. Runtime data stays in the relay's private config and cache roots. The phone stores its relay list locally. Relays never connect to one another, and no central service is required: a tunnel reaches the relay directly, and gateway mode uses the gateways you selected. ## Audit log Remote writes append private JSONL attempt and result records under `/audit/remote-writes.jsonl`. This includes agent input and lifecycle commands plus workspace and worktree creation, rename, reorder, close, open, and removal. An attempt and its result share a request id and correlate the phone client, WebSocket connection, target identifiers, and outcome; the attempt also records payload size and SHA-256 digest. Neither retains prompt, response, or upload content. A value answered at a no-echo terminal prompt is recorded by length alone — no digest, no keystrokes — so a low-entropy secret cannot be recovered from the trail; it is likewise absent from the activity journal. The file is mode `0600` inside a mode `0700` directory and rotates at 5 MiB with three retained rotations. ## End-to-end encryption When a relay key is configured, the phone and relay authenticate an ephemeral P-256 ECDH handshake with that key, derive per-connection keys with HKDF-SHA-256, and encrypt every subsequent WebSocket message with AES-256-GCM. The phone sends encrypted key confirmation before the relay registers the connection. The relay key stays in the QR/setup URL fragment and phone storage; it is never placed in the WebSocket URL or an HTTP header. Cloudflare — or, on the gateway transport, the gateway — can still observe connection metadata such as endpoints, timing, and encrypted frame sizes, but not relay commands, terminal output, uploads, or push-subscription details. Once the direct WebRTC path takes over, application frames and their metadata leave the gateway entirely; it still saw the rendezvous and the encrypted signaling that set the path up. Tokenless loopback development connections do not add application-layer encryption. As with any browser E2EE app, this assumes the phone is running trusted app code. A provider that actively replaces the JavaScript before it reaches the phone could capture the relay key; use an already installed app or an independently controlled app origin when that threat is in scope. Relay keys shorter than 16 bytes are rejected, but length alone is not entropy. The visible handshake proof permits offline guesses, so use the random, relay-unique key generated by setup rather than a human-chosen value. ## Device verification When the app's device-lock setting is enabled, it verifies the device before it will connect at open, and again on resume before the interface unlocks; see [mobile-app.md](mobile-app.md). A resume lock keeps the existing encrypted session open rather than dropping it, because the relay key lives in the browser's local storage and the unlock reconnects with it either way: dropping the connection would cost every resume a full re-dial without denying an attacker anything. Because that session stays live, the unlock screen covers the page rather than dimming it, and the open terminal stops streaming and stops leasing the computer's pane width until verification succeeds. ## Health endpoints - `GET /health` — process liveness; returns `ok`. - `GET /healthz` — version, revision, web bundle, instance, inventory state, and the `gateway` registration state on the hybrid transport. - `GET /readyz` — HTTP 200 once the listener and a Herdr inventory are ready, HTTP 503 before that.