Chapter 21 of 26
Web SSH Terminal
Overview
PatchMon ships an in-browser SSH terminal that lets operators connect to any monitored Linux, FreeBSD or Windows host without leaving the web UI. Windows hosts need the OpenSSH Server feature enabled first; see Windows hosts. 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:22on 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 Terminal tab on the host detail page is only shown to users whose role has can_use_remote_access.
Opening a terminal
- Go to Hosts and click the host you want to connect to.
- On the Host Detail page, open the Terminal tab (or click the SSH Terminal button in the header).
- Pick the Connection mode (Direct or Proxy).
- Enter the SSH username (defaults to
root, orAdministratoron Windows hosts; the last-used username per host is cached in your browser's local storage and takes precedence). - Choose an Authentication method:
- Password: type the host password.
- Key: paste the private key (OpenSSH or PEM format) and the passphrase if encrypted.
- Adjust the SSH port if needed (default
22). - If you picked Proxy mode, set the Proxy host (default
localhost) and Proxy port (default22). These are the destination the agent will dial, typicallylocalhost:22when you want the agent to SSH into its own host. - 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:
- Browser →
POST /api/v1/auth/ssh-ticketwith{ "hostId": "<id>" }. Requires your PatchMon session cookie. Returns a 30-second, single-use ticket. - Browser opens
wss://<patchmon-host>/api/v1/ssh-terminal/<hostId>?ticket=<ticket>. - Server consumes the ticket (deleted from Redis on use), validates the user is active and has permission, and upgrades to WebSocket.
- Browser sends the
connectmessage with auth credentials, terminal size, and connection mode. - Server dials
host.ip(falling back tohost.hostname) on the chosen port, authenticates with password or private key, and starts an interactive shell.
Host key verification (trust on first use): host keys are now always verified against a PatchMon-managed known_hosts file. The first time a host is contacted its key is recorded and the connection is accepted; on later connections a key that no longer matches is rejected as a possible man-in-the-middle and the session fails. The insecure "accept any key" behaviour has been removed. In the stock Docker image this file is not on a persistent volume, so recorded keys reset when the container is recreated and the next connection re-records them. To keep keys across upgrades, set PATCHMON_SSH_KNOWN_HOSTS to a path on a mounted volume.
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:
- Browser → ticket + WebSocket as in Direct mode.
- Server receives the
connectmessage withconnection_mode: "proxy". - 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. - The agent dials
<proxy_host>:<proxy_port>(defaultslocalhost:22) on its own host and pipes the stream back to the server over the WebSocket asssh_proxy_dataframes. For security,proxy_hostis restricted to the host's own registered address (its IP, hostname or interface address) orlocalhost; it cannot be used to reach other machines through the agent. - The server forwards those frames to the browser as terminal
dataevents.
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
localhostthrough the agent without punching holes through the edge firewall.
Windows hosts
The Terminal tab works on Windows hosts through the OpenSSH Server that ships with Windows. PatchMon does not install or enable it for you. On Windows hosts the tab shows a short reminder of this above the connection form.
Enable OpenSSH Server. Run in an elevated PowerShell on the host:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
If the feature is already installed, the first command changes nothing; you still need to start the service and set it to start automatically.
Allow port 22. Installing the feature normally creates the OpenSSH-Server-In-TCP firewall rule. Check, and create it if it is missing:
if (-not (Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -DisplayName "OpenSSH Server (sshd)" `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}
The firewall rule matters for Direct mode, where the PatchMon server dials the host. Proxy mode connects to localhost:22 from the agent on the host itself, so it needs only the service running and ssh-proxy-enabled: true in C:\ProgramData\PatchMon\config.yml.
Username. On Windows hosts the username defaults to Administrator. Any local administrator or domain account allowed to log on works; for a domain account use DOMAIN\user or user@domain. On Windows client editions (Windows 10 and 11) the built-in Administrator account is disabled by default, and on non-English installs it has a localised name (for example Administrateur), so sign in with your own administrator account instead. A username you have used before on that host is remembered and takes precedence.
Key authentication. For an account in the local Administrators group, Windows OpenSSH ignores the user's own ~\.ssh\authorized_keys and reads C:\ProgramData\ssh\administrators_authorized_keys instead. The file must be writable only by Administrators and SYSTEM, or sshd silently refuses the key:
Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value "ssh-ed25519 AAAA... you@example"
icacls.exe C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
Accounts that are not administrators use C:\Users\<user>\.ssh\authorized_keys as usual. Paste the matching private key into the SSH Key field in the Terminal tab.
Shell. The session opens in cmd.exe, the Windows OpenSSH default. Type powershell to switch for the rest of the session. To change the default for every SSH login on the host, set the DefaultShell value under HKLM:\SOFTWARE\OpenSSH on the host; PatchMon uses whatever shell sshd starts.
Terminal or RDP? The Terminal tab gives you a command line over SSH and works with password or key authentication. The RDP tab gives you the full Windows desktop through Guacamole and needs rdp-proxy-enabled on the agent instead. Use the terminal for quick commands and scripts, and RDP when you need the GUI.
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/randon 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 withInvalid or expired ticket. - Ticket validation also verifies the
hostIdin 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
exiton the remote shell), the terminal shows "SSH connection closed" and the WebSocket stays open for a potential newconnect. - 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. |
| Windows host: "connection refused" or a timeout | OpenSSH Server is not installed or sshd is stopped, or (Direct mode) port 22 is blocked by the Windows firewall. See Windows hosts. |
| Windows host: key is rejected but the password works | The account is a local administrator, so sshd reads C:\ProgramData\ssh\administrators_authorized_keys, not the user's own file. Check that file and its permissions. |
| "host key verification failed … (possible man-in-the-middle)" | The host presented a different SSH key than the one recorded on first use. If the host was legitimately rebuilt or its key rotated, remove its line from the PatchMon-managed known_hosts file and reconnect to record the new key. |
| "Proxy host must be the host's own address or localhost" | In Proxy mode the agent will only connect to the host's own registered address or localhost. Connecting to an unrelated address through the agent is blocked by design. |
Related pages
- AI Terminal Assistant
- RDP via Guacamole
- Managing the PatchMon Agent
- Agent Configuration Reference