Skip to main content
PathMon

Chapter 21 of 26

Web SSH Terminal

Updated Read the full guide

Overview

PatchMon ships an in-browser SSH terminal that lets operators connect to any monitored Linux/FreeBSD host without leaving the web UI. The terminal is a full xterm with line editing, colours, scrollback, resize, and keyboard shortcuts, powered by a WebSocket between the browser and the PatchMon server.

Two connection modes are supported:

  • Direct: the PatchMon server dials the host's SSH port (22 by default) and bridges the session. Use this when your server has network reach to the hosts.
  • Proxy: the PatchMon server asks the host's own agent to open a local SSH connection (to localhost:22 on the host) and tunnels it back through the agent's existing outbound WebSocket. No inbound SSH port exposure required on the target host.

Authentication to the host uses an SSH password or an SSH private key (with an optional passphrase). Authentication to PatchMon itself is handled by your existing session cookies plus a one-time ticket described below.

Web SSH is shipped in PatchMon from 1.4.0 onwards; in 2.0 it is provided under the remote_access capability module.

Permissions

Role Web SSH access
admin / superadmin Always granted.
Any other role Requires can_use_remote_access on the role permissions.

Users without can_use_remote_access attempting to open the terminal are rejected with HTTP 403 Access denied during the WebSocket handshake.

The Hosts related permission (can_manage_hosts) is required to see the "Open Terminal" control on the host detail page in the first place.

Opening a terminal

  1. Go to Hosts and click the host you want to connect to.
  2. On the Host Detail page, open the Terminal tab (or click the SSH Terminal button in the header).
  3. Pick the Connection mode (Direct or Proxy).
  4. Enter the SSH username (defaults to root; the last-used username per host is cached in your browser's local storage).
  5. Choose an Authentication method:
    • Password: type the host password.
    • Key: paste the private key (OpenSSH or PEM format) and the passphrase if encrypted.
  6. Adjust the SSH port if needed (default 22).
  7. If you picked Proxy mode, set the Proxy host (default localhost) and Proxy port (default 22). These are the destination the agent will dial, typically localhost:22 when you want the agent to SSH into its own host.
  8. Click Connect.

Once the green "SSH connection established" line appears, the terminal is live and interactive.

Your SSH credentials are never stored by the server or browser. They are sent over the authenticated WebSocket once at connect time and held in browser state only for the life of the session. Disconnecting clears them from memory.

Direct mode

In Direct mode, the PatchMon server dials the host directly:

  1. Browser → POST /api/v1/auth/ssh-ticket with { "hostId": "<id>" }. Requires your PatchMon session cookie. Returns a 30-second, single-use ticket.
  2. Browser opens wss://<patchmon-host>/api/v1/ssh-terminal/<hostId>?ticket=<ticket>.
  3. Server consumes the ticket (deleted from Redis on use), validates the user is active and has permission, and upgrades to WebSocket.
  4. Browser sends the connect message with auth credentials, terminal size, and connection mode.
  5. Server dials host.ip (falling back to host.hostname) on the chosen port, authenticates with password or private key, and starts an interactive shell.

Host key verification: the server uses ~/.ssh/known_hosts on the PatchMon container if it exists, and falls back to InsecureIgnoreHostKey otherwise. Direct mode does not prompt the user to accept host keys. Keys are accepted on first use when the fallback is active. Supply a known_hosts file via volume mount for production deployments that require strict verification.

Use Direct mode when:

  • The PatchMon server has network reach to the host on the SSH port.
  • You accept bridging SSH via the server host rather than via the host's agent.

Proxy mode

Proxy mode routes the SSH session through the host's existing agent WebSocket, avoiding the need to expose an SSH port inbound to PatchMon.

Flow:

  1. Browser → ticket + WebSocket as in Direct mode.
  2. Server receives the connect message with connection_mode: "proxy".
  3. Server generates a 16-byte session ID, stores a proxy session record, and sends { "type": "ssh_proxy", "session_id": …, "host": "localhost", "port": 22, "username": … } over the agent's existing WebSocket.
  4. The agent dials <proxy_host>:<proxy_port> (defaults localhost:22) on its own host and pipes the stream back to the server over the WebSocket as ssh_proxy_data frames.
  5. The server forwards those frames to the browser as terminal data events.

Agent config requirement. Proxy mode requires integrations.ssh-proxy-enabled: true in the agent's /etc/patchmon/config.yml. This setting is not pushed from the server. It has to be set manually and the agent service restarted. If the agent rejects the request, the terminal shows "Agent not connected" or an agent-supplied error.

Use Proxy mode when:

  • The host has no inbound SSH exposure (behind NAT, in a restricted VPC, behind a corporate firewall).
  • You already trust the agent's outbound connection to PatchMon and want to reuse it.
  • You want to SSH to localhost through the agent without punching holes through the edge firewall.

One-time tickets for WebSocket auth

WebSocket upgrades cannot include the normal authentication cookies reliably across all browsers, and passing long-lived tokens via query parameters would expose them in server logs and browser history. PatchMon avoids both problems with one-time tickets:

  • Tickets are 64 hex characters, generated from crypto/rand on the server.
  • Tickets live in Redis with a 30-second TTL.
  • A ticket carries the user ID and the host ID it was minted for.
  • The WebSocket handler consumes the ticket on first use (atomic DEL). A second attempt to open a WebSocket with the same ticket fails with Invalid or expired ticket.
  • Ticket validation also verifies the hostId in the URL matches the one encoded in the ticket. Stolen tickets cannot be reused against a different host.

You get the ticket implicitly by clicking Connect in the UI; there is no operator-visible ticket string.

Keyboard and terminal interactions

The embedded xterm supports the usual shortcuts:

Action Shortcut
Copy selection Browser-standard (Ctrl+Shift+C / Cmd+C)
Paste Ctrl+Shift+V / Cmd+V
Send Ctrl+C to the remote Ctrl+C (when no selection)
Scrollback Mouse wheel or trackpad
Clear screen Remote clear command

The terminal automatically resizes when the PatchMon browser window resizes, the AI Assistant panel opens/closes, or the sidebar collapses. The server is notified over the WebSocket so the remote TTY keeps cols and rows in sync. Resize events are honoured in Direct mode (when supported by the remote SSH server) and in Proxy mode via ssh_proxy_resize messages to the agent.

Terminal output is also captured in a rolling 5 000-character buffer for the AI Terminal Assistant, if enabled.

Session lifetime and idle timeout

  • Ticket TTL: 30 seconds. A session that takes longer than that to start must re-request a ticket.
  • Idle disconnect: after 15 minutes with no terminal activity the session is closed automatically. A visible warning appears 1 minute before the disconnect. Any input or output resets the timer.
  • Manual disconnect: click Disconnect in the toolbar or close the terminal panel. Credentials are wiped from browser state on disconnect.

What happens when the WebSocket drops

  • If the server-side SSH process exits (e.g. you type exit on the remote shell), the terminal shows "SSH connection closed" and the WebSocket stays open for a potential new connect.
  • If the WebSocket itself drops unexpectedly and you were connected, the terminal attempts to reconnect once after 3 seconds, via a brand-new ticket and WebSocket. Authentication re-uses the cached username but you must re-enter password or key, as credentials are not persisted in the browser.
  • Close codes that are not retried: 1000 (normal close), 1006 (abnormal close, often auth failure), 1008 (policy violation). For those, you get "Connection failed: Session may have expired. Please refresh the page or log in again.".

Auditing

Every successful ticket mint (i.e. the user has requested a terminal session) fires an ssh_session_started event:

  • Severity: informational (configurable in Alert Lifecycle).
  • Metadata: host_id, host_name, user_id.
  • Reference: the host record.

Route this event type to a destination (for example, a #security Discord channel) if you want a live audit trail of all web SSH sessions. Configure routing in Notification Routes and Delivery Log.

Server logs also record each upgrade and ticket consumption under ssh-terminal connected and ssh-terminal ticket invalid log lines.

Troubleshooting

Symptom Likely cause and fix
"Authentication required. Please log in again." when clicking Connect Your PatchMon session cookie is missing or expired. Refresh the page and sign in.
"Invalid or expired ticket" on upgrade More than 30 seconds elapsed between ticket mint and WebSocket open, or the ticket was already consumed. Retry; PatchMon mints a new ticket on the retry.
"Agent not connected." in Proxy mode Host's agent WebSocket is down. Verify from Host Detail → Status; restart the agent service on the host.
Agent rejects with "ssh-proxy-enabled must be true" Set integrations.ssh-proxy-enabled: true in the agent's config.yml and restart the agent service.
"Failed to parse private key" Key is encrypted: add the passphrase. Or the key format is unsupported; use OpenSSH or PEM PKCS#8.
Connection established but first-time host key warning on server log The PatchMon container has no known_hosts for this host. Add one via a volume mount, or accept that first-use keys are auto-trusted.