Chapter 22 of 26
RDP via Guacamole
Known issue (2.0.0). The RDP connection flow has a known bug in PatchMon 2.0.0. Sessions may fail to establish, disconnect early, or return opaque errors in certain environments. A fix is planned for the next release. See Release Notes 2.0.0 for details. If RDP is mission-critical for your rollout, validate the workflow in a staging instance before relying on it in production.
Overview
PatchMon 2.0 lets you open a full RDP session to a Windows host from your browser, with no RDP client installed locally and no inbound RDP port exposed to the outside world. The session travels:
- From the browser as a Guacamole WebSocket tunnel, into the PatchMon server.
- From the PatchMon server into a
guacdsidecar, which speaks the RDP protocol. - From
guacdthrough a short-lived TCP proxy to the host's own PatchMon agent, which forwards tolocalhost:3389on the Windows host.
This is a one-time ticketed connection with keyboard, mouse, and clipboard support. Screen size is configurable, and NLA/TLS/legacy RDP is auto-negotiated.
RDP is provided under the remote_access capability module.
Known issue: 2.0.0 RDP bug
Before using RDP in production, read this.
Version 2.0.0 has a known bug in the RDP connection flow that can cause:
- Sessions to fail handshake with generic errors.
- Ticket resolution issues that surface as "invalid or expired ticket" on otherwise valid sessions.
- Early disconnects after a successful handshake under some network conditions.
A fix is planned for the next release. In the meantime:
- If RDP is critical, stay on the last 1.4.x release that works for you, or retry the session.
- Always verify the Web SSH Terminal works end-to-end first. It has no such known issue and is a good baseline check for connectivity and agent health.
- When reporting RDP issues, include the server log lines tagged
rdp-ticketandrdp tunnel, and the agent'srdp_proxy_*log lines.Full context: Release Notes 2.0.0, section Known issues.
Architecture
┌──────────┐ Guacamole protocol over WSS ┌──────────────────┐ TCP 4822 ┌───────┐ TCP ┌──────────────────┐ TCP 3389 ┌──────────────┐
│ Browser │ ───────────────────────────→ │ patchmon-server │ ───────────→ │ guacd │ ────→ │ Ephemeral port │ ──────────→ │ Windows │
│ (guac- │ │ (Go binary) │ │ │ │ on the server │ │ Agent relay │
│ common- │ ←─────────────────────────── │ │ ←─────────── │ │ ←──── │ (local listen) │ ←────────── │ → localhost │
│ js) │ │ │ │ │ │ │ │ 3389 │
└──────────┘ └──────────────────┘ └───────┘ └──────────────────┘ └──────────────┘
│ ▲
│ Agent WebSocket (rdp_proxy_*) │
└─────────────────────────────────────────────────┘
Key components:
guacd: Apache Guacamole's daemon, shipped as a sidecar container in PatchMon's Docker Compose (guacamole/guacd:1.6.0). Runs on4822/tcpinside thepatchmon-internalnetwork. No public ports.- PatchMon server: acts as the Guacamole WebSocket tunnel endpoint and owns the RDP ticket store. It asks the host's agent to set up a local TCP proxy, then hands that proxy to
guacd. - Agent proxy: on receiving
rdp_proxyover its WebSocket, the agent opens a local TCP bridge and forwards bytes between PatchMon andlocalhost:3389on the Windows host. Requiresintegrations.rdp-proxy-enabled: truein the agent config. - Windows host: runs the standard Windows RDP service on
127.0.0.1:3389(bound to localhost via the agent; no inbound exposure needed).
See Installing PatchMon Server on Docker for the sidecar configuration as deployed by the standard compose file. If you run PatchMon without the sidecar, install guacd separately (apt install guacd / yum install guacd) and set GUACD_ADDRESS to point at it.
Permissions and module
| Access | Requirement |
|---|---|
| Open an RDP session | admin, superadmin, or can_use_remote_access + can_view_hosts on your role |
| Create RDP ticket for a host | can_manage_hosts (needed to see the control on the host detail page in the first place) |
| Deployment | remote_access capability module enabled |
Users without the required permission are rejected at POST /auth/rdp-ticket with 403 Access denied.
Prerequisites
Before you can open an RDP session to a host, all of these must be true:
- The host is identified as Windows in PatchMon (
os_typeorexpected_platformcontains "windows"). Non-Windows hosts are rejected with400 RDP is only available for Windows hosts. - The host's PatchMon agent is online and connected via its WebSocket.
- The agent's
config.ymlhasintegrations.rdp-proxy-enabled: true. This setting is not pushable from the server. You edit it on the host and restart thePatchMonAgentservice. - RDP is enabled on the Windows host, and the agent's user context can reach
localhost:3389. (The default NLA mode is fine; PatchMon negotiates security automatically.) - The PatchMon server is able to reach
guacdat its configured address (defaults to127.0.0.1:4822orguacd:4822depending on deployment). If not, RDP ticket creation fails early with503 guacd is not reachable on the PatchMon server.
Opening an RDP session
- Go to Hosts and click the Windows host.
- On Host Detail, open the Remote Access area and click Open RDP (or the RDP icon in the toolbar).
- Enter the Windows username and password for the account to sign in as.
- Optionally adjust the Screen size. Defaults to
1024 × 768. Allowed range is320–8192on each axis; values outside this range are clamped. - Click Connect.
The server then:
- Preflights
guacdwith a 2-second TCP dial. - Asks the rdpproxy to allocate an ephemeral listener (one per session), which is the "port" that
guacdwill dial into. - Sends
rdp_proxyto the host's agent, waits up to 12 seconds for the agent to acknowledge withrdp_proxy_connected. - Mints an RDP ticket and returns the WebSocket tunnel URL to the browser.
- The browser opens
wss://<patchmon-host>/api/v1/rdp/websocket-tunnel?ticket=…&width=…&height=…, completes the Guacamole handshake, and starts streaming frames.
Once connected you see the Windows sign-in screen (or desktop, if NLA authenticated) in your browser.
Credentials handling
- Username and password are sent once over HTTPS to
POST /auth/rdp-ticketand stored encrypted in the RDP ticket record alongside the session ID, host ID, and screen dimensions. - The ticket is single-use and expires quickly (tens of seconds). When
guacdconsumes it to set up the tunnel, the stored credentials are forwarded toguacdand then to RDP. PatchMon itself does not keep them after the session starts. - For environments where Windows asks for a certificate, the default guacd config uses
ignore-cert=trueto accept the self-signed certificate Windows generates out of the box, matchingmstsc.exebehaviour. Hardened per-host overrides are a candidate for a future release. - Security mode is negotiated as
any, which lets FreeRDP pick the strongest common mode (NLA → TLS → legacy RDP). Hardcoding NLA would break hosts with Negotiate/TLS-only security layers and refuses blank-credential sessions. - Empty username and password are allowed (some hosts accept blank-credential sessions) but
guacdusually fails handshake in that case; the server logsmissing_username_or_password: trueso you can spot it in the audit trail.
One-time tickets
RDP tickets work like SSH tickets:
- 64-character hex string,
crypto/randentropy. - Stored in Redis with a short TTL.
- Consumed atomically on first use by
doGuacConnect. - Bound to a user ID, a host ID, a proxy session ID, a port, the encrypted credentials, and the requested screen width/height.
- Validated against the user's current active state. A deactivated user cannot re-use a still-live ticket.
You never see or handle the ticket directly; the UI requests it under the hood when you click Connect.
Keyboard layouts and clipboard
- Keyboard: the Guacamole client maps the browser's keydown events to scancodes. For most Latin keyboards (en-GB, en-US) this "just works". For non-Latin layouts, match the Windows layout to the browser's. Guacamole has no per-session keyboard-layout selector in PatchMon 2.0.
- Clipboard: bidirectional text clipboard is supported via Guacamole's native clipboard channel. Copy in Windows, paste in the browser, or vice versa. Rich clipboard (images, file lists) is not supported.
- Mouse: primary, secondary, and wheel. Mouse-wheel-click middle button is supported.
- Full-screen: toggle via your browser's F11/fullscreen mode. Guacamole resizes the RDP session to the browser viewport where the host allows dynamic resolution.
Printer redirection, audio, drive mapping, and USB forwarding are not enabled in 2.0.
Session limits
| Limit | Default | Source |
|---|---|---|
| Concurrent RDP sessions per server | 50 | rdpproxy.DefaultMaxSessions |
| Per-session idle timeout | 30 minutes | rdpproxy.sessionIdleTimeout |
guacd preflight timeout |
2 seconds | guacdPreflightTimeout |
| Agent handshake timeout | 12 seconds | agentHandshakeTimeout |
Exceeding the concurrency cap returns 503 Too many concurrent RDP sessions on this server, please try again later.
Disconnecting
- Manual disconnect: close the browser tab or click the disconnect control in the RDP panel. The server tears the session down, tells the agent to close the TCP bridge, and releases the Redis ticket record.
- Windows sign-out: the RDP session closes normally; the tunnel stays open for a brief grace period before cleanup.
- Idle close: after 30 minutes of no data flow the session is killed server-side.
Auditing
Every successful RDP ticket creation fires an rdp_session_started event:
- Severity:
informational. - Metadata:
host_id,host_name,user_id. - Reference: the host record.
Route this event type in Notification Routes and Delivery Log if you want a live audit trail of who is signing into Windows hosts from PatchMon.
Server logs include rdp-ticket and rdp session opened lines with the session ID, user ID, host ID, negotiated security posture, and a missing_username_or_password field. Use these to triage incidents; the session ID ties everything together.
Troubleshooting
| Symptom | Response from the server | Likely cause and fix |
|---|---|---|
guacd is not reachable on the PatchMon server. |
503, code: guacd_unavailable |
The sidecar is not running. Check docker compose ps guacd, or install guacd on the host and set GUACD_ADDRESS. |
The PatchMon agent on this host is not connected. |
503, code: agent_disconnected |
Agent is offline. Start / restart the PatchMonAgent service on the host. |
The PatchMon agent did not respond to the RDP proxy request in time. |
504, code: agent_timeout |
The agent is connected but its handler is stuck, or blocked by firewall. Check agent logs for rdp_proxy entries. |
rdp proxy is not enabled (via rdp-proxy-enabled) |
502, code: agent_rdp_disabled |
Set integrations.rdp-proxy-enabled: true in the agent config.yml and restart the agent. |
| invalid host | 502, code: agent_invalid_host |
Proxy host format rejected (reserved for future per-target proxies). |
| connection refused / no route to host on port 3389 | 502, code: rdp_port_unreachable |
RDP is not running on the Windows host, or a local firewall blocks localhost:3389. Enable RDP on the host. |
RDP is only available for Windows hosts |
400 |
Non-Windows host. Use the Web SSH Terminal instead. |
Forbidden: origin not allowed in WebSocket upgrade |
403 |
Your browser's Origin header isn't in PatchMon's CORS_ORIGIN allow-list. Update CORS_ORIGIN (or the dynamic origin resolver) to include your PatchMon URL and restart. |
| Guacamole handshake fails repeatedly with a valid user and password | Check rdp tunnel guacd handshake failed in the server log. This is the 2.0.0 known-issue scenario; consult the release notes and retry. |
Related pages
- Web SSH Terminal
- AI Terminal Assistant
- Installing PatchMon Server on Docker
- Release Notes 2.0.0