Repository navigation
Server-side credential handling: a design frame for the validate-on-receipt deployment class #1357
Description
IronRDP's server stack has grown several credential- and transport-handling threads, each scoped to a concrete PR or issue. Together they describe one coherent design axis, and naming it explicitly would let the pieces land as a single shape rather than independently. This is in the spirit of the architecture-direction issues you've started writing down (#1349, #1352), applied to the server's credential and transport surface.
Two server deployment classes
ironrdp-server today serves the credential-injection / proxy class well: the server holds the credentials and either exact-matches them at the acceptor or runs CredSSP via a pre-loaded CredentialsProxy. Devolutions Gateway is the canonical consumer, driving ironrdp_acceptor's CredSSP (server role, facing the client) alongside ironrdp_connector's CredSSP (client role, facing the target) to inject vault credentials at both ends. For that class the model is exactly right.
The second class is validate-on-receipt: a standalone or infrastructure RDP server that authenticates the connecting party itself, an end user against PAM/LDAP/AD or a backend access layer against a capability token. The spec permits this and locates it precisely:
- credentials arrive in the Client Info PDU (MS-RDPBCGR 2.2.1.11) on the Standard (§5.3.2) and Enhanced/TLS (§5.4.1) security paths, after the channel is secured;
- CredSSP/NLA (MS-CSSP) is a pre-session challenge-response that needs a password-equivalent secret in hand, so it structurally cannot validate against an opaque backend;
- MS-RDP leaves server-side authorization policy to the implementer, so validate-on-receipt is in-spec;
ironrdp-serverjust doesn't name it as a supported shape yet.
This need recurs from outside Lamco as well: #508 ("How can I check RDP credentials?") was answered with the stop-after-NLA / CredSSP route, which is the injection/mechanism path and only works when the secret is already known.
The in-flight pieces are instances of the second class
- Credential validation — Support server-side credential validation (e.g. PAM) via CredentialValidator trait #1154 / feat(server): add CredentialValidator trait for server-side auth #1172 (
CredentialValidator): validate Client Info PDU credentials on the TLS-mode path. The validation-policy counterpart to the injection path; different security modes, not competing mechanisms (the tell:pam+hybridis incompatible by construction). The safe-default-reject design also addresses the explicit-opt-in-for-no-auth concern raised on fix(acceptor): skip credential check when server credentials are None #1150. - Pre-authenticated transport — feat(server): accept connections with TLS terminated at a lower layer #1281 (
run_connection_pre_authenticated): accept a stream whose TLS was terminated by an outer layer (WSS / RDCleanPath) for proxy-fronted backends. The constraint is concrete: the closed PR feat(server): add PreSecured transport mode for pre-encrypted connections #1210 showed that advertisingPROTOCOL_SSLwhile skipping the handshake breaks vanilla clients, so the pre-auth path must not touch X.224 negotiation and relies on RDCleanPath as the "TLS happened above" signal. - Conformant server protocol behind a proxy — feat(server): expose NetworkAutoDetect RTT via a shared handle #1346 / feat(acceptor): negotiate the MCS message channel #1347 / fix(pdu)!: send NetworkAutoDetect over the MCS message channel #1348 (NetworkAutoDetect over the MCS message channel, message-channel negotiation): correct server behavior the backend/proxied deployment depends on.
- Runtime-agnostic serving (longer-term):
ARCHITECTURE.mdstates the I/O invariant ("doing I/O is not allowed for these crates") and listsironrdp-client,ironrdp-web, andironrdp-asyncas the I/O-permitted crates, yetironrdp-serveris the most I/O-coupled crate in the workspace and is absent from that list. Extending the existing sans-I/O discipline (core tier + the client-side-tokio/-blocking/-futuressplit) to the server would make deterministic testing, alternative runtimes, and the WebSocket+RDCleanPath path natural. This is gated behind the in-flightanyhow-> typed-error migration (ironrdp-server: replace anyhow::Result with typed error in public API #1209), so it is a later step, noted here for coherence rather than proposed now.
The design frame
Treat validate-on-receipt as a recognized server deployment class, so these threads are designed against one frame rather than piecemeal:
- credential validation is async and runtime-agnostic (the hook fires once per connection, so the cost is negligible and the blocking offload belongs in the implementor), and object-safe for optional pluggability;
- rejection is spec-defined (
ServerSetErrorInfoPdu) across both the exact-match and validator paths, so behavior does not diverge by path; - the transport seam (pre-authenticated / RDCleanPath) and the validation seam compose, so a single
RdpServercan serve direct-TLS clients and proxy-fronted backends; - none of this touches the injection / CredSSP path, which the proxy class keeps using unchanged.
This is not a proposal to redirect IronRDP's architecture. It is a frame that makes the existing PRs coherent and gives one place to discuss the server's I/O posture as a deliberate choice. #1172 is the immediate concrete step; I'm answering the open design questions there and pointing back here for the broader shape.
Activity
CBenoit commentedon Jul 7, 2026
Apologies for the delay in responding. The synthesis is useful and mostly holds. Position below, with one factual correction first because it reshapes the proposal, in your favor, as it happens.
The correction being: the canonical consumer of
ironrdp-serveris not Devolutions Gateway.Devolutions Gateway depends on
ironrdp-pdu,-core,-rdcleanpath,-tokio,-connector, and-acceptor. It drivesCredsspSequencedirectly in its RDP proxy for credential injection; it does not use theironrdp-servercrate at all. So the injection/proxy class doesn't live inironrdp-server, it lives at the acceptor/connector layer, where Devolutions Gateway assembles it by hand.An early driver for
ironrdp-serveris originally qemu-display (https://gitlab.com/marcandre.lureau/qemu-display) by Marc-André Lureau (@elmarco), and that's the closest thing it has to a canonical consumer, and it is a standalone server serving a VM's display, not a proxy injecting known credentials. The in-house consumer today is Devolutions Agent (but still an experimental, non working prototype), and your Lamco server and wrdp are two more. Every one of them is a standalone server.This simplifies your proposal rather than weakening it. The dividing line isn't "injection/proxy vs validate-on-receipt inside one crate", but instead it's that the injection/proxy class lives at the acceptor/connector tier, while
ironrdp-serverhas always been the standalone-server crate. What varies among its consumers is the credential model: qemu-display sits at one end (no end-user authentication to speak of), and validate-on-receipt. Authenticating the connecting party against PAM/LDAP/AD is the more demanding end of that same axis.So recognizing validate-on-receipt isn't adding a second class to a crate built for the first, validate-on-receipt is a supported credential model for
ironrdp-server's standalone-server surface. The principles you list are already the ones we've been applying in the merged work, so I'm happy to make them explicit as review criteria for this surface:CredentialValidator, and nowConnectionBinderin feat(server)!: post-authentication connection binding for multi-user servers #1413), so a server that doesn't authenticate (qemu-display) pays nothing and a server that does (PAM/LDAP) plugs in cleanly;ServerSetErrorInfoPdu) on every path, exact-match and validator alike, so behavior does not diverge by path;TransportTls) and the validation seam compose;We should capture the crate-role distinction in the docs: acceptor/connector as building blocks for the proxy/injection class,
ironrdp-serveras the batteries-included standalone server across the range from no-auth to validate-on-receipt.The provenance question you raised on #1413 is the right one, and it's the same gap the binder gate hits from the other side. I'm requiring a credential-origin tag on
AcceptorResult(ClientInfovsCredSspDelegated) in that PR before merge. One note on framing: "pam + hybrid incompatible by construction" is true for authentication but not for authorization. Once CredSSP has authenticated the principal, a validator running authorization policy on it is coherent, and #1413's delegated-credential hand-off is exactly that pattern. So the resolution is provenance visibility, not suppressing the validator on Hybrid. Your comment there already pointed the same direction.The sans-I/O restructuring of
ironrdp-serveris a real discussion, but it's a separate and much larger decision. When #1209 lands we can take it up on its own merits in its own issue.I'll keep this issue open as the anchor for the credential/transport surface. Concrete changes should keep arriving as scoped PRs against it, the way #1413 did.
glamberson commentedon Aug 19, 2026
Thanks for the correction, and no need to apologize for the delay. The Gateway assumption was mine (now very stale) and it does change the shape: If injection/proxy lives at the acceptor/connector tier where Gateway builds it by hand, then ironrdp-server was never split between two classes, it's one standalone-server crate with a credential-model range, and validate-on-receipt just sits at the demanding end of that range alongside qemu-display at the permissive end. That's a cleaner frame than what I proposed and I'll work from it going forward.
Agreed on keeping this open as the anchor. On the sans-I/O side, we already covered that ground on Matrix a couple days ago; looking forward to you codifying the plan in more detail in the near future, as mentioned there. #1209 is still the prerequisite and I'll keep it green while it sits in review.
I'm going through our open issues here right now since it's been a while since the last pass, so you'll probably see a few more replies land on older threads over the next bit.