Skip to content

Server-side credential handling: a design frame for the validate-on-receipt deployment class #1357

Open

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-server just 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

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 RdpServer can 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

  1. CBenoit commented on Jul 7, 2026

    @CBenoit
    Member

    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-server is not Devolutions Gateway.

    Devolutions Gateway depends on ironrdp-pdu, -core, -rdcleanpath, -tokio, -connector, and -acceptor. It drives CredsspSequence directly in its RDP proxy for credential injection; it does not use the ironrdp-server crate at all. So the injection/proxy class doesn't live in ironrdp-server, it lives at the acceptor/connector layer, where Devolutions Gateway assembles it by hand.

    An early driver for ironrdp-server is 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-server has 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:

    • validation/binding hooks are async, object-safe, and optional (CredentialValidator, and now ConnectionBinder in 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;
    • rejection is spec-defined (ServerSetErrorInfoPdu) on every path, exact-match and validator alike, so behavior does not diverge by path;
    • the transport seam (TransportTls) and the validation seam compose;
    • the acceptor/connector layer remains the building-block tier that the proxy class consumes directly.

    We should capture the crate-role distinction in the docs: acceptor/connector as building blocks for the proxy/injection class, ironrdp-server as 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 (ClientInfo vs CredSspDelegated) 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-server is 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.

  2. glamberson commented on Aug 19, 2026

    @glamberson
    ContributorAuthor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions