Skip to content

feat(connector): add opt-in standard RDP security support#1201

Closed
Dietmar Maurer (maurerdietmar) wants to merge 2 commits intoDevolutions:masterfrom
maurerdietmar:standard-security-support
Closed

feat(connector): add opt-in standard RDP security support#1201
Dietmar Maurer (maurerdietmar) wants to merge 2 commits intoDevolutions:masterfrom
maurerdietmar:standard-security-support

Conversation

@maurerdietmar
Copy link
Copy Markdown

Allow connections using standard RDP security (no TLS, no CredSSP) by adding enable_standard_rdp_security to Config. When enabled and the server selects standard RDP security, the connector skips the enhanced security upgrade and proceeds directly to basic settings exchange.

This is useful when the transport is already secured by other means (e.g., a TLS WebSocket proxy or SSH tunnel).

Allow connections using standard RDP security (no TLS, no CredSSP) by
adding `enable_standard_rdp_security` to `Config`. When enabled and the
server selects standard RDP security, the connector skips the enhanced
security upgrade and proceeds directly to basic settings exchange.

This is useful when the transport is already secured by other means
(e.g., a TLS WebSocket proxy or SSH tunnel).

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
@CBenoit
Copy link
Copy Markdown
Member

Benoît Cortier (CBenoit) commented Apr 7, 2026

Hello!

I’m surprised by the simplicity of this.

The documentation / spec about the RDP Standard Security does not mention a non-encrypted mode like described in this PR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f1c7c93b-94cc-4551-bb90-532a0185246a

It is my understanding that the RDP Standard Security uses an old, broken encryption algorithm called RC4.

Maybe the intention here is to support something completely different that is not the RDP Standard Security, but rather a custom, non standard transport?

@glamberson
Copy link
Copy Markdown
Contributor

Greg Lamberson (glamberson) commented Apr 7, 2026

Interesting PR.

I face the exact same topology problem: RDP server behind a hypervisor's TLS WebSocket proxy, where the last hop is already secured by the infrastructure.

I agree with Benoît Cortier (@CBenoit) that the naming needs a closer look. Per [MS-RDPBCGR] Section 5.3, Standard RDP Security is actually a specific protocol with RC4 encryption and RSA key exchange. It is not "no security." PROTOCOL_RDP (0x00000000) selects that RC4-based mechanism, not an unencrypted mode. So enable_standard_rdp_security reads like it is enabling a security protocol when it is actually bypassing one.

Even setting the naming aside, Standard RDP Security (the actual RC4-based protocol) exists in the spec mainly for backward compatibility with very old deployments. Implementing it fully would mean adding RC4 key exchange, which is a lot of work for a mechanism the industry has been moving away from. I am not sure adding either version of this path to the library is the best long-term investment.

For what it is worth, I solved this use case differently in my own Proxmox RDP console stack (lamco-qemu-rdp and related components, currently in alpha). The guest-to-host hop uses vsock, which is hypervisor-local and never hits a network. RDP with full TLS/CredSSP is spoken only on the client-facing side. This way the library's security model stays untouched and the deployment topology handles the trust boundary instead.

@maurerdietmar
Copy link
Copy Markdown
Author

The documentation / spec about the RDP Standard Security does not mention a non-encrypted mode like described in this PR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f1c7c93b-94cc-4551-bb90-532a0185246a

It is in section 5.3.2:

If the server determines that no encryption is necessary for the session, it can send the client a value of zero for the selected Encryption Method and Encryption Level. In this scenario the Security Commencement phase of the connection sequence...

It is my understanding that the RDP Standard Security uses an old, broken encryption algorithm called RC4.

This PR does not implement RC4 encryption. Instead, it allows the connector to accept PROTOCOL_RDP (0x0) during X.224 negotiation and proceed with ENCRYPTION_METHOD_NONE / ENCRYPTION_LEVEL_NONE - meaning no encryption at the RDP protocol layer at all. The spec defines this as a valid combination (when both fields are 0, the server random and certificate are absent, and no security exchange occurs).

Maybe the intention here is to support something completely different that is not the RDP Standard Security, but rather a custom, non standard transport?

no.

The confusing part in the specs is the last sentence of section 5.3.2:

To protect the confidentiality of client-to-server user data, an RDP server ensures that the negotiated Encryption Level is always greater than zero when using Standard RDP Security mechanisms.

IMHO, the zero/zero path is there for use cases where data is already encrypted. I tested this and it works with mstsc.exe and xfreerdp3.

…ryption_level_none

Use spec terminology (MS-RDPBCGR 5.3.2) to clarify that this option
permits ENCRYPTION_LEVEL_NONE / ENCRYPTION_METHOD_NONE, rather than
implying full Standard RDP Security (RC4) support.
@glamberson
Copy link
Copy Markdown
Contributor

Thanks for the response, and the rename to allow_encryption_level_none is much clearer. Let me dig into the spec a bit more though, because I think the full picture is more nuanced.

The two passages in Section 5.3.2 look contradictory but actually serve different purposes. The passage you cited describes the wire mechanics: what happens IF the server sends zero/zero. The protocol machinery is defined: Security Commencement is skipped, no Security Exchange PDU, no server random or certificate, no security headers on regular PDUs.

But the very next sentence is a normative constraint:

"To protect the confidentiality of client-to-server user data, an RDP server ensures that the negotiated Encryption Level is always greater than zero when using Standard RDP Security mechanisms."

Microsoft's Open Specifications Support Team blog post "Encryption Negotiation in RDP connection" (December 7, 2011) goes further: "It seems that theoretically it is possible to get the unencrypted traffic from client to server if the Encryption Level is set to zero. But in order to protect the confidentiality of client-to-server user data, an RDP server MUST set the Encryption Level to be greater than zero as specified in 5.3.2 of MS-RDPBCGR."

The spec defines the zero/zero mechanics because that combination is mandatory under Enhanced RDP Security (Section 5.4.1): when TLS or CredSSP provides the encryption, the RDP layer sets both fields to zero to avoid double-encryption. That is the intended and only spec-conformant use of ENCRYPTION_LEVEL_NONE. Under bare PROTOCOL_RDP with no external security protocol, zero/zero is mechanically parseable but normatively prohibited.

Regarding the interop testing: xfreerdp3 does accept ENCRYPTION_LEVEL_NONE from a server. FreeRDP's gcc_read_server_security_data() validates the NONE/NONE combination, sets UseRdpSecurityLayer = FALSE, skips certificate and random parsing, and proceeds. No special client flags are needed, and both v2 and v3 handle it identically.

For mstsc.exe, I'd be curious about the exact topology you tested. Windows has no Group Policy or registry setting that allows encryption level zero under Standard RDP Security. The minimum configurable value is Low (1). If mstsc is connecting through a TLS WebSocket proxy, then from its perspective the connection is Enhanced RDP Security via the proxy's TLS layer, and ENCRYPTION_LEVEL_NONE in the GCC data is the expected, correct behavior. That is a different code path from what this PR enables on the IronRDP client side, which is accepting bare PROTOCOL_RDP with no external security at the RDP protocol level.

For this kind of deployment topology, there are ways to get there without the spec-violating combination. The proxy or infrastructure could present the connection as Enhanced RDP Security to the client. If the client negotiates TLS with the proxy endpoint, the server-side ENCRYPTION_LEVEL_NONE becomes the correct, spec-conformant response. The proxy handles the security boundary, and the client and server both follow the spec. Alternatively, the client could negotiate TLS/CredSSP at the RDP layer even behind the proxy. This is defense-in-depth with a slight performance cost, but the library's security model stays completely intact regardless of the transport.

One of IronRDP's real strengths compared to FreeRDP is that it's a clean, spec-conformant implementation without years of accumulated workarounds for non-conforming servers. There's real value in preserving that. FreeRDP accepts this path because it's a monolithic client that has to interoperate with everything, but IronRDP has the opportunity to be more principled about what it accepts. I think it's worth exploring whether the infrastructure layer can solve the security boundary problem rather than relaxing the library's protocol conformance.

@glamberson
Copy link
Copy Markdown
Contributor

Following up on the spec discussion. I've been looking at this from the server side too, and I think there's a cleaner path for both sides.

ironrdp-server already has RdpServerSecurity::None, which is the server-side equivalent of what this PR adds to the client. It advertises PROTOCOL_RDP with ENCRYPTION_LEVEL_NONE, the same spec-violating combination we discussed above. So the library is already inconsistent with itself on this.

Rather than relaxing the client to accept the spec violation, I think the better fix is on the server side. I've prototyped a RdpServerSecurity::PreSecured variant that solves the same deployment problem (pre-secured transport) without the spec issue:

PreSecured advertises PROTOCOL_SSL during X.224 negotiation, so the client sees Enhanced RDP Security. It skips the actual TLS handshake since the transport is already encrypted. The GCC Server Security Data contains ENCRYPTION_LEVEL_NONE, which is the correct and spec-conformant response under Enhanced RDP Security (MS-RDPBCGR Section 5.4.1).

From the client's perspective, a PreSecured server looks like a normal TLS connection. No client-side changes needed. The zero/zero in the GCC data is expected and correct because the client believes an External Security Protocol is in effect.

This covers the WebSocket proxy topology, vsock, SSH tunnels, and any other pre-secured transport without exposing a plaintext PROTOCOL_RDP path. I'll submit it as a separate PR shortly.

@maurerdietmar
Copy link
Copy Markdown
Author

First, many thanks for you help. I tested your pre-secured patch, but xfreerdp3 fails now:

when UNIX-CONNECT:/var/run/test.rdp is the rdp server using pre-secured:

# socat TCP-LISTEN:13390,reuseaddr,fork UNIX-CONNECT:/var/run/test.rdp

# xfreerdp3 /v:127.0.0.1:13390 /cert:ignore /u:xxx /p:xxx 
[10:18:34:151] [350780:00055a3d] [WARN][com.freerdp.client.x11] - [load_map_from_xkbfile]:     : keycode: 0x08 -> no RDP scancode found
[10:18:34:152] [350780:00055a3d] [WARN][com.freerdp.client.x11] - [load_map_from_xkbfile]:     : keycode: 0x5D -> no RDP scancode found
[10:18:34:162] [350780:00055a3d] [ERROR][com.freerdp.crypto] - [freerdp_tls_handshake]: BIO_do_handshake failed
[10:18:34:162] [350780:00055a3d] [ERROR][com.freerdp.core] - [transport_default_connect_tls]: ERRCONNECT_TLS_CONNECT_FAILED [0x00020008]

And when I tunnel the connection with:

# socat OPENSSL-LISTEN:13390,reuseaddr,fork,cert=server.pem,verify=0 UNIX-CONNECT:/var/run/test.rdp

# xfreerdp3 /v:127.0.0.1:13390 /u:xxx /p:xxx 
[10:42:07:443] [360605:0005809e] [WARN][com.freerdp.client.x11] - [load_map_from_xkbfile]:     : keycode: 0x08 -> no RDP scancode found
[10:42:07:443] [360605:0005809e] [WARN][com.freerdp.client.x11] - [load_map_from_xkbfile]:     : keycode: 0x5D -> no RDP scancode found
[10:42:07:453] [360605:0005809e] [ERROR][com.freerdp.core.transport] - [transport_read_layer]: BIO_read returned a system error 104: Connection reset by peer
[10:42:07:453] [360605:0005809e] [ERROR][com.freerdp.core] - [transport_read_layer]: ERRCONNECT_CONNECT_TRANSPORT_FAILED [0x0002000D]
[10:42:07:464] [360605:0005809e] [ERROR][com.freerdp.core.transport] - [transport_read_layer]: BIO_read returned a system error 104: Connection reset by peer
[10:42:07:464] [360605:0005809e] [ERROR][com.freerdp.core] - [transport_read_layer]: ERRCONNECT_CONNECT_TRANSPORT_FAILED [0x0002000D]
[10:42:07:464] [360605:0005809e] [ERROR][com.freerdp.core] - [freerdp_connect]: freerdp_post_connect failed

socat reports:

2026/04/09 10:42:07 socat[360680] E SSL_accept(): error:0A00010B:SSL routines::wrong version number

Seems I miss-understand something?

@glamberson
Copy link
Copy Markdown
Contributor

Thanks for testing this. You're right, PreSecured doesn't work. Advertising PROTOCOL_SSL triggers a mandatory TLS handshake per MS-RDPBCGR, and there's no mechanism to signal "TLS is already done" at the protocol level. I was wrong about this approach. Please accept my heartiest apologies for the wild goose chase.

Looking at it more carefully, the spec constraint I cited ("ensures > 0") uses softer language than I initially presented. The zero/zero wire mechanics are fully specified and all major clients handle them. FreeRDP has shipped the same pattern for years via the LocalConnection flag with the same rationale (local sockets, SSH tunnels, port forwarding).

I thought I was fixing a longstanding problem, but I've just been confusing the isse.

I'll close PR #1210. Your approach with the opt-in flag and security warning is the right answer for this use case.

Thanks.

@maurerdietmar
Copy link
Copy Markdown
Author

I uploaded a new version in PR #1214 (merged both patches, better naming, no functional changes)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants