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

Serge Caron 65 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

Answer accepted by question author
VPHAN 44,620 Reputation points Independent Advisor
2025-12-19T17:36:53.1433333+00:00

Hi Serge Caron,

How was the issue? It seems your previous answer has been deleted. I could not read it.

VP

Was this answer helpful?

1 person found this answer helpful.
0 comments No comments

44 additional answers

Sort by: Most helpful
  1. VPHAN 44,620 Reputation points Independent Advisor
    2025-12-17T04:19:05.4166667+00:00

    Hi Serge Caron,

    So you have eliminated the configuration and logic errors, leaving us with a behavioral constraint of the KDC Proxy service itself. (we have no choice but to try and eliminate each possibility until we sort out the possible one, but thanks for being patient with me).

    So I guess the "Missing Piece" is a combination of Local Domain Optimization and the Split-Brain DNS validation check.

    The KDC Proxy service (KpsSvc) contains logic that checks if the requested Realm matches the Local Domain of the server it is running on. If it matches, it typically ignores the Registry Mapping (KdcNames) entirely, assuming that the operating system's native locator (DsGetDcName) is the most reliable authority for its own domain. This explains why your Event 309 message remains "strictly identical" regardless of your script changes—the registry key is effectively dead code for the local domain.

    The Validation Trap: When KpsSvc falls back to the native locator (DNS Discovery), it retrieves the DC's Hostname (e.g., dc01.mydomain.tld). Before opening the socket, the service (or the underlying networking stack) often resolves this hostname to ensure validity.

    The Split-Brain Failure: In your "Same Name" environment (Internal AD = External DNS), if the RD Gateway resolves the DC's FQDN to its Public IP (due to the external DNS zone), the KDC Proxy detects that it is about to send the request back out to the internet (or to itself/VIP), triggers a loop detection/security check, and aborts the connection. This causes the "Rediscover" retry loop (Event 309) and eventual downgrade to NTLM.

    The HOSTS File Override is the solution here. Since we cant force the Registry Key to take precedence for the Local Domain, we must ensure that the "Native Discovery" path resolves correctly. You must force the RD Gateway to resolve the DC's FQDN to its Private IPv4 Address at the system level. On the Server 2022 RD Gateway (Member Server):

    Open C:\Windows\System32\drivers\etc\hosts in Notepad (as Administrator).

    Add an entry for your Domain Controller(s):

    192.168.x.x dc01.mydomain.tld

    192.168.x.x mydomain.tld

    (Replace 192.168.x.x with the actual Internal IPv4 of the DC, and use the exact FQDN appearing in Event 309).

    Flush DNS: ipconfig /flushdns

    Restart KpsSvc: Restart-Service KPSSVC

    When the KDC Proxy falls back to standard discovery (ignoring your registry key), it finds the DC name. It then resolves that name using the OS resolver. The hosts file intercepts this resolution, returning the Internal IP immediately. This satisfies the validation check (internal IP = valid route), allowing the TCP packet on Port 88 to proceed.

    One final check: global catalog (port 3268) The error realm\UPN (e.g., MYDOMAIN.TLD*@MYDOMAIN.TLD) on the logon screen is a visual artifact of the RD Gateway (Member Server) failing to authenticate the UPN via NTLM. On a Member Server, validating a UPN login (*@domain.tld) often requires connectivity to the Global Catalog (TCP 3268) to map the UPN to a SID. Ensure your RD Gateway can reach the DC on TCP 3268 in addition to Port 88.

    Was this answer helpful?

    0 comments No comments

  2. Serge Caron 65 Reputation points
    2025-12-17T01:44:49.8333333+00:00

    Hello VPHAN,

    The following code summarizes the last few answers. If the internal RDG name differs from the FQDN on the certificate, use the FQDN of the DCs otherwise, use the IPv4 address. Then, prune out any DC that is filtering Kerberos traffic on port 88.

    		### Explicit KDC Mapping (Critical for Non-DC Gateways)
    		Try { Get-ADDomainController -Identity $RDG -ErrorAction Stop | Out-Null }
    		Catch {
    			### $DomainName = (Get-WmiObject Win32_ComputerSystem).Domain.ToUpper()
    			$DomainName = $env:USERDNSDOMAIN.ToUPPer()
    			$KdcProxyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\KPSSVC\Settings\KdcProxy\$DomainName"
    			If (-not (Test-Path $KdcProxyPath)) { New-Item -Path $KdcProxyPath -Force | Out-Null }
    			# Get actual DCs for the domain
    			If ( $PrivateExposure -ne $PublicExposure ) {
    				$DCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
    			} else {
    				# the Proxy service sometimes de-prioritizes FQDNs that match the local domain suffix to
    				# avoid conflicts with the OS's native Kerberos locator : use the DC's IP
    				$DCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty IPv4Address
    			}
    			# Use only reachable KDCs
    			$LiveKDCs = @()
    			ForEach ($DC in $DCs) { If (Test-NetConnection -ComputerName $DC -Port 88) { $LiveKDCs += $DC} }
    			New-ItemProperty -Path $KdcProxyPath -Name "KdcNames" -Type MultiString -Value $LiveKDCs -Force
    		}
    

    Unfortunately, although it is definitely progress, this has no bearing on the issue.

    The message in Event ID 309 is strictly identical to what is was using the FQDN of the RDG.

    Note: the single DC and the single RDG are both VMs in the same host (which has none of these roles). The DNS server parameter of the RDG is manually set to the IP of the DC.

    Same piece missing ;-(

    Regards,

    Was this answer helpful?

    0 comments No comments

  3. VPHAN 44,620 Reputation points Independent Advisor
    2025-12-16T14:58:21.7833333+00:00

    Hi,

    Sr for the syntax error in my previous logic, the missing backslash in the string concatenation $KdcProxyBase$IncomingRealm would indeed create a malformed key path like ...KdcProxyMYDOMAIN.TLD instead of ...KdcProxy\MYDOMAIN.TLD.

    However, after you corrected it and it still failed made me think that the issue is likely not the Registry Key name itself (which creates the route), but the Value inside KdcNames (the destination).

    In a split-brain scenario where Internal AD = mydomain.tld and External DNS = mydomain.tld:

    1/ The Client asks for mydomain.tld.

    2/ The Proxy finds your Registry Key for mydomain.tld.

    3/ The Proxy reads KdcNames. Your script populated this with FQDNs (e.g., dc1.mydomain.tld).

    4/ The Failure: The Member Server (RDG) attempts to resolve dc1.mydomain.tld.

    If it mistakenly resolves to the Public IP (due to cached entries, public forwarders, or the "Split-Brain" nature of the zone), the Proxy sees it is about to send traffic to itself (or the external firewall VIP). It detects a loop and aborts the specific route, falling back to the "Rediscover" (DNS) events you are seeing. Even if it resolves to the internal IP, the Proxy service sometimes de-prioritizes FQDNs that match the local domain suffix to avoid conflicts with the OS's native Kerberos locator (DsGetDcName).

    => So the solution is to bypass DNS with IP Addresses

    To sidestep the DNS/Name ambiguity entirely, you must configure the KdcNames value using the Internal IP Addresses of your Domain Controllers, not their names. This forces the KDC Proxy to treat the destination as a distinct network endpoint rather than "part of the namespace."

    Here is the corrected logic to apply to your Server 2022 (Member Server). This script explicitly grabs the IPv4 address of the DC and uses that for the mapping.

    FIX: Force IP-Based Routing for Split-Brain DNS

    $DomainName = (Get-WmiObject Win32_ComputerSystem).Domain

    Note: We use the exact casing returned by WMI to ensure alignment, though Registry is technically case-insensitive.

    $KdcProxyBase = "HKLM:\SYSTEM\CurrentControlSet\Services\KPSSVC\Settings\KdcProxy"

    $TargetKey = "$KdcProxyBase$DomainName"

    Ensure the key exists

    If (-not (Test-Path $TargetKey)) {

    New-Item -Path $TargetKey -Force | Out-Null

    Write-Host "Created KdcProxy Key for $DomainName"

    }

    Get the Internal IPv4 Addresses of the DCs (Skip IPv6/Link-Local)

    We filter for 'Enabled' and 'IPv4' to get the raw IP string.

    $DCIPs = Get-ADDomainController -Filter * |

    Select-Object -ExpandProperty IPv4Address

    Write-Host "Configuring KDC Proxy to bypass DNS and use IPs: $DCIPs"

    Write the IPs to the registry.

    KpsSvc will now try TCP 88 to these IPs directly.

    New-ItemProperty -Path $TargetKey -Name "KdcNames" -Type MultiString -Value $DCIPs -Force

    Write-Host "Configuration Updated. Restarting KpsSvc..."

    Restart-Service KPSSVC

    When KdcNames contains an IP address:KpsSvc skips the DNS Resolution step for the target. It opens a TCP socket directly to <IP>:88. This bypasses the "Am I talking to myself?" check that often triggers when dc1.mydomain.tld is involved in a split-brain naming war. It successfully forwards the KRB_AS_REQ to the internal DC.

    1 final check: Ensure that your Server 2022 Member Server has a firewall rule allowing Outbound TCP Port 88 to the Domain Controllers. While member servers usually have this, strictly locked-down DMZ servers sometimes block direct Kerberos TCP calls, forcing the OS to try UDP (which KDC Proxy handles poorly in this specific manual-mapping scenario).

    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

    Was this answer helpful?

    0 comments No comments

  4. Serge Caron 65 Reputation points
    2025-12-16T14:29:59.1366667+00:00

    Hello VPHAN,

    In your December 14 reply, you specified the realm in uppercase as a subkey of the base KdcProxy key:

    ### Explicit KDC Mapping (Critical for Non-DC Gateways) $DomainName = (Get-WmiObject Win32_ComputerSystem).Domain.ToUpper() $KdcProxyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\KPSSVC\Settings\KdcProxy\$DomainName"
    

    In your latest answer, the domain name in uppercase is concatenated to the "KdcProxy" string, thus creating an entirely new key:

    $KdcProxyBase = "HKLM:\SYSTEM\CurrentControlSet\Services\KPSSVC\Settings\KdcProxy"
    $AliasPath = "$KdcProxyBase$IncomingRealm"
    $OfficialDomain = (Get-WmiObject Win32_ComputerSystem).Domain.ToUpper()
    $OfficialPath = "$KdcProxyBase$OfficialDomain"
    
    

    I did create the alias using the former approach. Your code made me realize the important caveat here.

    The external domain name (public certificate) and the internal domain name (AD) are the same.

    In the script, the SPNs are restricted with the test "If ( $PrivateExposure -ne $PublicExposure ) {..."

    Obviously, creating the alias key as your code suggests did not solve my issue ;-). The registry keys are not case sensitive. I did remove the key after the test with the UPN specified in uppercase and then in lower case.

    I have only two of these old domains (25 years+) where the AD name is the same as the public DNS name : this was the way to do things in the late 1990s.

    Is there a way to sidestep this condition ?

    Regards,

    Was this answer helpful?

    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.