WebSocket (Authenticated)
Topology Updates
GET /api/v1/ws/topology
Upgrades to WebSocket. The osprey_access cookie is validated during the upgrade. Real-time topology diffs are pushed to connected clients when devices or links change. Connection limits: 100 total concurrent connections, 10 per user. Excess connections are rejected with HTTP 503.
Protocol: Binary or text frames containing JSON topology diff messages.
SSH/Telnet Terminal Proxy
GET /api/v1/ws/ssh?host={router_id}
Upgrades to WebSocket and proxies an SSH or telnet session to the specified device.
Roles: admin or engineer. Operators are refused with 403; interactive device access is outside the read-only role boundary.
Query parameters:
host (required) — Router ID (IP address) of the device, as stored in the topology. /32 suffix is automatically stripped.
Authentication flow:
- Client sends a JSON text message with the username only (the password is typed interactively, not sent here):
{"type": "auth", "username": "admin"}
- Server dials SSH (port 22) and relays the device's banner and password prompt to the terminal as binary frames; the user types the password, which is read from the client's binary input frames (SSH keyboard-interactive, password-callback fallback). On a wrong password the device re-prompts (bounded retries). On a connection-level failure, if the
ssh.allow_telnet_fallback system setting is true, falls back to telnet (port 23) — disabled by default (cleartext credentials).
- Server sends connection status and device output as binary frames.
- After login, binary frames carry terminal I/O in both directions.
- Client sends resize events as JSON text messages:
{"type": "resize", "cols": 80, "rows": 24}
SSRF Protection:
- Host must exist in the topology (validated via
GetDeviceByRouterID). This topology whitelist is the primary SSRF protection.
- The
host you pass is the device's router-id; the server then resolves the actual dial target via resolveDialHost — the device's selected reachable management IP (management_ip / device_address), falling back to the router-id only when no management IP has been selected. The SSH host-key (TOFU) pin is keyed on this dialed address, not the router-id.
- After resolution, the dial target is blocked if it falls in a non-routable / infrastructure range: loopback (
127.0.0.0/8, ::1/128), link-local (169.254.0.0/16, fe80::/10), multicast (224.0.0.0/4, ff00::/8), reserved / class-E (240.0.0.0/4), and the 0.0.0.0/8 "this network" range.
- RFC 1918 private ranges (10/8, 172.16/12, 192.168/16) are NOT blocked because topology devices legitimately use private IP addressing.
Error responses:
400 Bad Request — Missing host parameter
401 Unauthorized — Missing or invalid access token
403 Forbidden — Caller is an operator, host is not in topology, resolved dial target is in a blocked IP range, or the SSH terminal proxy is globally disabled via the security.disable_ssh_proxy system setting
WebSocket close codes:
1000 (Normal Closure) — Session ended normally
1008 (Policy Violation) — Invalid auth message or missing credentials, or an SSH host key mismatch (close reason host key mismatch)
Host key verification (TOFU): The proxy pins each device's SSH host key on first connect (ssh_known_host table). On a later mismatch the connection is refused (close 1008, reason host key mismatch). Before closing, the server emits one text control frame (terminal output is sent as binary frames, so a text frame is an unambiguous control message) describing the change:
{"type":"hostkey_mismatch","host":"198.51.100.13","port":22,
"stored_fingerprint":"SHA256:…","stored_key_type":"ssh-ed25519",
"offered_fingerprint":"SHA256:…","offered_key_type":"ssh-ed25519"}
An admin can then clear the pin (see Clear SSH Host Key) and reconnect, which re-runs TOFU and pins the new key.
Clear SSH Host Key (TOFU)
DELETE /api/v1/admin/ssh/known-hosts?host={host}&port={port}&offered_fp={fingerprint}
Admin only. Removes the pinned TOFU host key for host:port so a legitimately re-keyed or replaced device can be reconnected to — the next SSH connection re-runs Trust-On-First-Use and pins the new key. Recorded in the audit log (action=clear, entity_type=ssh_known_host) with the cleared fingerprint and the client-reported offered fingerprint.
Query parameters:
host (required) — device host/IP exactly as pinned. The proxy keys the TOFU pin on the device's management IP (the reachable address it actually dials, resolved via resolveDialHost), falling back to the router-id only when the device has no selected management IP — so pass whichever address was dialed, as it appears in the mismatch control frame's host field.
port (optional, default 22).
offered_fp (optional) — the fingerprint the device most recently presented; recorded in the audit detail as client-reported context.
Response: 200 OK
{"cleared": true, "host": "198.51.100.13", "port": 22, "fingerprint": "SHA256:…", "key_type": "ssh-ed25519"}
Error responses:
400 Bad Request — missing host
403 Forbidden — caller is not an admin
404 Not Found — no key pinned for that host:port