CommandRelay treats remote terminal control as high-risk and defaults to read-only behavior.
- Prevent unauthorized access to active terminal sessions.
- Prevent unauthorized or silent command injection.
- Preserve operator auditability for sensitive actions.
- Constrain blast radius when a client or network path is compromised.
- New WebSocket clients start with
inputEnabled=false. inputis accepted only when both client input is enabled and global kill switch is off.- Non-authenticated clients are blocked from non-
authevents whenCOMMANDRELAY_AUTH_TOKENis configured. - Startup validation requires
COMMANDRELAY_AUTH_TOKENfor non-loopback bind addresses. - Token validation uses timing-safe comparison (
timingSafeEqual) with equal-length gate. - Input path is guarded by per-client rate limit, max bytes, and attached-pane checks.
- Sensitive actions (
auth_ok,auth_fail,attach,enable_input,disable_input,input) are audit logged. - Pane input ownership arbitration is enforced; first successful writer claims pane lane until release or explicit takeover.
- HTTP route surface includes exact
GET /health; with static hosting enabled,GET /andGET /appredirect (308) to/app/, which serves static assets. - Static serving is rooted at
COMMANDRELAY_APP_STATIC_DIR(apps/webdefault), with traversal-protection and404for missing/forbidden paths. - Non-matching HTTP paths return
404, and non-/wsWebSocket upgrades are rejected. - Auth for web clients is message-based (
auth.payload.token), not header-based. - When auth token mode is enabled, all non-
authevents are blocked withauth_requireduntil successful auth.
- The bridge accepts raw text input only (
input.payload.data). - Newline-separated payloads are executed as Enter-separated tmux sends.
- There is no server-side symbolic key translation layer for web clients.
- Security controls apply before dispatch: auth gate, input-enabled policy, pane attachment, byte limit, and input rate limit.
| Threat Area | Concrete Attack | Current Mitigation | Residual Risk / Operator Action |
|---|---|---|---|
| Auth gate | Send attach/input before auth completes |
Server returns error.code=auth_required for non-auth events until authenticated |
If running open mode (no token on loopback), rely on host/network isolation |
| Auth token guessing and timing attacks | Brute-force auth.token or exploit compare timing |
Static token is required on non-loopback host; compare uses timing-safe equality and rejects different lengths | Static bearer token has no built-in expiry/rotation; rotate operationally |
| Token disclosure in logs | Leak token through audit/event logging | Auth failures log reason (invalid_token) but not the submitted token value |
Keep process/stdout logs private; avoid reverse proxies that log frames |
| Bi-directional input injection | Malicious client sends unsolicited input |
Input requires: authenticated client, explicit enable_input, kill switch off, attached pane, rate limit pass, payload size <= COMMANDRELAY_MAX_INPUT_BYTES |
Attached pane trust is session-scoped, not user-scoped ACL |
| Replay on input channel | Re-send old input envelope with new transport session |
No protocol nonce or anti-replay token for input; request correlation is best-effort via requestId |
Use short-lived network paths and review audit hashes for suspicious duplicates |
| Replay/output gap confusion | Client reconnects with stale lastSeq |
Server replays bounded in-memory history (maxHistoryEvents), then falls back to snapshot if precise range unavailable |
History is ephemeral and per-watcher; exact gap reconstruction is not guaranteed |
| Concurrent writers | Multiple clients/tabs enable input on the same pane | Server enforces pane input ownership and rejects non-owner writes with input_lane_conflict unless takeover is requested and allowed |
Use explicit handoff policy; keep override disabled in higher-risk deployments |
| Kill switch bypass attempt | Client calls enable_input while global kill switch is on |
enable_input cannot override global flag; policy remains inputEnabled=false, and input returns input_disabled |
Kill switch is process config, not dynamic remote toggle |
COMMANDRELAY_INPUT_KILL_SWITCHis parsed once at startup (true/falsestrict parser).- When on,
enable_inputemitspolicy_updatewithinputEnabled=falseandglobalInputDisabled=true. inputcontinues to be rejected witherror.code=input_disabled.disable_inputalways forces client input state to false.disconnectclears attached panes and resets client input state.
- Treat each browser/app tab as an independent client with its own
clientIdand policy state. - For a shared pane, designate exactly one writer tab/client; keep all others read-only (
disable_input). - Writer handoff sequence: current writer sends
disable_inputand detaches/disconnects; next writer sendsenable_input. - Emergency takeover uses
inputwithoverride=true(ortakeOwnership=true) whenCOMMANDRELAY_ALLOW_INPUT_OVERRIDE=true. - If unexpected commands appear, restart bridge with
COMMANDRELAY_INPUT_KILL_SWITCH=onto freeze all input, then re-establish one-writer operation. - Use audit logs to trace
enable_input/disable_input/inputevents per client during incident review.
- Bind to loopback or private mesh only; avoid direct public exposure.
- Set and rotate
COMMANDRELAY_AUTH_TOKEN; do not reuse across environments. - Enable
COMMANDRELAY_AUDIT_LOGand protect log file access. - Keep
COMMANDRELAY_MAX_INPUT_BYTESand rate limits conservative. - Consider
COMMANDRELAY_ALLOW_INPUT_OVERRIDE=offfor stricter multi-tab control. - Treat kill switch as emergency brake and verify policy state from
policy_update.