With Server 2025, how do you avoid NTLM authentication for a RDP session originating from a non domain workstation ?

Serge Caron 0 Reputation points
2025-12-06T19:48:13.38+00:00

In order to reduce complexity, I am using a test domain consisting of a single domain controller ON PREMISES and a single user, the domain administrator.

There is a single role installed: Remote Desktop Gateway. None of the other 5 RDS roles are installed. Direct Access and/or VPNs are not allowed in this test.

The AD domain is MyDomain.local and the internal FQDN is MyServer.MyDomain.local

There is a Let's Encrypt certificate installed on this server for MyServer.MyDomain.TLD.

Finally, there is a single port 443 forwarded to the server from the firewall.

I can RDP into this server from the Internet to MyServer.MyDomain.local via RDG MyServer.MyDomain.TLD.

However, all logons are downgraded to NTLMv2 even if I set

rdgiskdcproxy:i:1

kdcproxyname:s:MyServer.Mydomain.TLD

In this test case, I am using a non domain joined Windows 10 Pro client (even if I know it is deprecated): we need to demonstrate RDG working with BYOD, including non Windows devices.

Is there a way to do this in Windows Server 2025 ?

Windows for business | Windows Server | Directory services | User logon and profiles
0 comments No comments
{count} votes

13 answers

Sort by: Most helpful
  1. VPHAN 10,640 Reputation points Independent Advisor
    2025-12-12T03:11:28.3866667+00:00

    Hi Serge Caron,

    You've done a great job!! However, the solution is staring us in the face within the setspn output you provided. If you look closely at the command you ran and the subsequent output, the issue is not a technical mystery of Windows Server 2025, but a simple syntax oversight during the registration attempt.

    In your console output, you executed: setspn -S HTTP/MyServer.MyDomain.local MYSERVER. The system correctly responded with "Duplicate SPN detected, operation aborted" because, as shown in the dump lines immediately preceding it, HTTP/MyServer.MyDomain.local already exists on the object.

    Crucially, HTTP/MyServer.MyDomain.TLD is completely missing from that list.

    The RD Gateway / KDC Proxy authentication process is extremely strict regarding the "Front Door." When the client connects, it does not care about the internal AD name yet; it is talking to the public URL defined in the gateway settings. The client requests a ticket for HTTP/MyServer.MyDomain.TLD. Active Directory looks at the CN=MYSERVER object, sees HTTP/Internal, TERMSRV/Internal, and HOST/Internal, but it does not find HTTP/ExternalTLD. Consequently, it returns 0x7 (Principal Unknown), and the client correctly aborts the Kerberos exchange.

    You must run the command specifically for the external address. Please open the Administrator Command Prompt on your DC and run: setspn -S HTTP/MyServer.MyDomain.TLD MYSERVER

    Once you run this, verify it immediately with setspn -L MYSERVER. You must see the TLD variant explicitly listed in the output. As soon as that entry appears, your Windows 10 client, with its current registry configuration, will successfully obtain the service ticket for the proxy, establish the tunnel, and then proceed to obtain the TERMSRV ticket for the RDP session.

    I hope you've found something useful here. If it helps you get more insight into the issue, it's appreciated to accept the answer then. Should you have more questions, feel free to leave a message. Have a nice day!

    VP

    0 comments No comments

  2. Serge Caron 0 Reputation points
    2025-12-12T21:35:27.0833333+00:00

    Hello VPHAN,

    I have a glass full / glass empty type of result.

    Once the SPN for the HTTP/fqdn is created for MYSERVER, the RDP client connects sucessfully.

    Before I go on, I have beefed up the Poor Man's config script for these clients and will provide a final version when this can be reused for anyone who has this issue. See below ;-)

    Now, I do not see any event in the server's NTLM Audit Log for a successful logon and I do see the details of the connection in the Remote Destop Gateway Console.

    I also do not see any event in the Security-Kerberos Operational Log (LogLevel is set to 1 on this servver): somehow, I find this disturbing knowing that a ticket was granted.

    I made sure that the only user in this domain (Administrator) is a member of the Protected Users group and redid the tests: all OK.

    Now, for the empty part: there are no tickets received by the client. Obviously, RDP does not create a endpoint on the network and the client will not communicate directly with any ressource on the network other than the RDG. So, are we chasing a Kerberos ticket that will never be delivered ?

    Regards,

    Here is the beefed up script (it can suffer some beautyfying ;-):

    $Realm = Read-Host "Please enter the remote Active Directory domain name (not the NetBIOS domain name)"
    $KdcFQDN =  Read-Host "Please enter the fully qualified domain name (FQDN) of the Remote Desktop Gateway"
    ### Make sure proper case is used in these namespaces
    $KdcFQDN = $KdcFQDN.ToLOWER()
    $Realm = $Realm.ToUPPER()
    ### Warn if IIS is not reachable on $KdcFQDN
    If ( -Not $(Test-NetConnection -ComputerName $KdcFQDN -Port 443) ) {
    	Write-Warning "HTTPS is not enabled on $KdcFQDN"
    }
    ### Location of Kerberos keys
    $KerberosLSA = "HKLM:\SYSTEM\CurrentControlSet\Control\LSA\Kerberos"
    $KerberosPolicies = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters"
    ### Delete this realm and dump actual configuration
    ksetup /DelHostToRealmMap ".$Realm" "$Realm"	# /RemoveRealm will not remove duplicate mappings
    ksetup /RemoveRealm $Realm
    ksetup /DumpState
    ## Host to Realm
    	ksetup /AddHostToRealmMap ".$Realm" "$Realm"
    	If ( (Get-ItemProperty -Path "$KerberosLSA\HostToRealm\$Realm").SpnMappings.Count -eq 2 ) {
    		If ( ((Get-ItemProperty -Path "$KerberosLSA\HostToRealm\$Realm").SpnMappings[0] -ceq ".$Realm") `
    				-and ("" -eq (Get-ItemProperty -Path "$KerberosLSA\HostToRealm\$Realm").SpnMappings[1] ) ) {
    				Write-Host "Standard mapping for $Realm"
    		} else { Write-Warning "Registy entries for $Realm mapping not managed by ksetup" }
    	} else { Write-Warning "More than one mapping defined for $Realm" }
    ### KDC
    	ksetup /addkdc "$Realm" $KdcFQDN
    	If ( (Get-ItemProperty -Path "$KerberosLSA\Domains\$Realm").KdcNames.Count -eq 2 ) {
    		If ( ((Get-ItemProperty -Path "$KerberosLSA\Domains\$Realm").KdcNames[0] -ceq $KdcFQDN) `
    				-and ("" -eq (Get-ItemProperty -Path "$KerberosLSA\Domains\$Realm").KdcNames[1]) ) {
    				Write-Host "Standard KDC setup for $Realm"
    		} else { Write-Warning "Registy entries for $Realm KDC not managed by ksetup" }
    	} else { Write-Warning "More than one KDC defined for $Realm" }
    ### Encryption Types
    	$EncTypes = ksetup /Domain $Realm /SetEncTypeAttr AES-256-CTS-HMAC-SHA1-96 AES-128-CTS-HMAC-SHA1-96
    	Try { Get-ItemProperty -Path "$KerberosLSA\Domains\$Realm" -Name SupportedEncryptionTypes -ErrorAction Stop  | `
    				Select-Object -ExpandProperty SupportedEncryptionTypes | Format-Table
    	}
    	Catch {	Write-Warning "ksetup failed to create encryption attributes for $Realm"
    			New-ItemProperty -Path "$KerberosLSA\Domains\$Realm" -Name "SupportedEncryptionTypes" -PropertyType DWORD -Value 24 -Force | `
    				Select-Object SupportedEncryptionTypes | Format-Table -HideTableHeaders
    	}
    	
    	If ( (Get-ItemProperty -Path "$KerberosLSA\Domains\$Realm").SupportedEncryptionTypes -ne `
    			(Get-ItemProperty -Path "$KerberosPolicies").SupportedEncryptionTypes ) {
    				Write-Warning "Encryption types for $Realm do not match default Kerberos policies for this computer."
    			}
    ### LogLevel
    	Write-Host "Kerberos Log Level", (Get-ItemProperty -Path "$KerberosLSA\Parameters").LogLevel
    ### Final configuration
    ksetup /DumpState
    
    0 comments No comments

  3. VPHAN 10,640 Reputation points Independent Advisor
    2025-12-13T02:14:42.92+00:00

    Hi Serge Caron,

    The behavior you are observing regarding the missing tickets and logs is actually expected behavior for this specific architecture (Workgroup client to Domain KDC Proxy), and the fact that your connection succeeds with the user in the Protected Users group is the definitive proof that Kerberos is working.

    Let’s clarify the "Empty Glass" mystery. A user in the Protected Users group is strictly prohibited from authenticating via NTLM, Digest Authentication, or Credential Delegation (CredSSP) using cached credentials. If the negotiation had downgraded to NTLMv2, the Domain Controller would have rejected the authentication request with a status of STATUS_ACCOUNT_RESTRICTION or STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT, and your RDP client would have shown an "Authentication Failed" error. Since you connected successfully, the authentication payload was indisputably Kerberos.

    The reason you do not see the tickets in klist on the client is due to Logon Session isolation. On a domain-joined machine, the TGT is held in the user's primary logon session and is visible to all processes. On a workgroup machine using KDC Proxy, mstsc.exe (the RDP client) acts as its own authentication silo. It performs the pre-authentication, obtains the TGT and the Service Ticket for HTTP/MyServer.MyDomain.TLD, and establishes the tunnel. These tickets are often scoped specifically to the process ID or a temporary logon session (LUID) created just for that connection handle, rather than the interactive desktop session where you are running cmd.exe. Once the RDP session is established, those tickets are either purged or reside in a memory space that the default klist command (which queries the current interactive session cache) does not display. You aren't chasing a ghost; you are looking for a ticket that was used and consumed by a specific process handle.

    Regarding the server logs, standard Kerberos auditing can be incredibly noisy. Unless you have enabled "Audit Kerberos Service Ticket Operations" to success in the Advanced Audit Policy Configuration, the DC will not log every ticket issuance (Event ID 4769). However, the absence of Event ID 4624 (Logon) with Authentication Package: NTLM is the positive confirmation you were looking for.

    Your script is excellent. The logic you implemented to handle the SupportedEncryptionTypes via New-ItemProperty in the Catch block is the correct technical workaround. The ksetup executable has a longstanding bug where it attempts to query domain policy objects via RPC that do not exist on a standalone SAM, leading to the 0xc0000034 error. Direct registry injection into HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Domains is the only reliable way to force AES negotiation on non-domain joined clients. This script effectively solves the "BYOD" gap for secure RDP over HTTPS.

    I hope you've found something useful here. If it helps you get more insight into the issue, it's appreciated to accept the answer. Should you have more questions, feel free to leave a message. Have a nice day!

    VP

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.