~/2025/09/17/powershell-windows-server-harden-rdp-without-breaking-access.md

PowerShell: Windows Server – Harden RDP Without Breaking Access

---
author: 
date: 
read: 5 min
in:   [ps, scripts]
tags: [powershell, windows]
---

$ grep -n '^#' post.md

RDP is still the most common way I end up on a Windows Server box, and it's also one of the first things I harden on a fresh build, because the defaults are more permissive than they need to be. Network Level Authentication is on by default on current Windows Server releases, but the security layer defaults to Negotiate (which can fall back to native RDP encryption with no server authentication), sessions can sit idle or disconnected forever, and the Remote Desktop firewall rules accept any source address. Locking RDP down to the point where you lock yourself out is a real risk, so this script leans on an explicit allowlist instead of guessing, checks for Group Policy that would override it, and is meant to be run from an address that's already on that allowlist.

Requirements

  • Windows Server 2016 or later, elevated PowerShell (Administrator).
  • Remote Desktop already enabled. The built-in "Remote Desktop" firewall rule group exists on every Windows Server install; RDP for administration is part of the base OS, not the RDS role.
  • The built-in NetSecurity module for the firewall changes, and the root\cimv2\TerminalServices WMI namespace (present by default) for the Group Policy check.
  • Clients that support NLA (Remote Desktop Connection 6.0 or later) and TLS 1.2. Every supported Windows client does.
  • Your management network's address ranges, and a console you can reach without RDP (iDRAC, iLO, hypervisor console) the first time you run it.

Parameters

NameTypeRequiredDescription
AllowedSourcestringYesIP addresses or CIDR ranges allowed to reach RDP. Must include the range you are connecting from.
IdleTimeoutMinutesintNoIdle time before an active session is disconnected. Default 30.
DisconnectedSessionTimeoutMinutesintNoHow long a disconnected session is kept before it's ended. Default 60.
DisableDriveRedirectionswitchNoBlocks client drive mapping (and clipboard file copy) into sessions.
DisableClipboardRedirectionswitchNoBlocks clipboard sharing between client and session.
AllowRestrictedAdminswitchNoLets clients connect with Restricted Admin mode or Remote Credential Guard (DisableRestrictedAdmin = 0).
DisableLegacyTlsswitchNoDisables the TLS 1.0 and TLS 1.1 server protocols in Schannel. Affects every TLS service on the host; restart required.

Usage

Harden RDP for a management subnet with the default session limits:

powershell
.\Set-HardenedRdpConfiguration.ps1 -AllowedSource "10.10.5.0/24"

A jump host that only ever needs a terminal: tighter limits, no drive or clipboard redirection, Restricted Admin allowed:

powershell
.\Set-HardenedRdpConfiguration.ps1 -AllowedSource "10.10.5.0/24", "10.10.6.10" -IdleTimeoutMinutes 15 -DisconnectedSessionTimeoutMinutes 30 -DisableDriveRedirection -DisableClipboardRedirection -AllowRestrictedAdmin

Sample output:

text
WARNING: Rule 'Allow 3389 - legacy' (enabled, inbound, TCP 3389) is not in the Remote Desktop group and still allows RemoteAddress Any. Review it; this script does not change it.

UserAuthentication SecurityLayer MinEncryptionLevel DisableRestrictedAdmin
------------------ ------------- ------------------ ----------------------
                 1             2                  3                      0

MaxIdleTime MaxDisconnectionTime fDisableCdm fDisableClip
----------- -------------------- ----------- ------------
     900000              1800000           1            1

RuleName                                  RemoteAddress
--------                                  -------------
Remote Desktop - User Mode (TCP-In)       {10.10.5.0/255.255.255.0, 10.10.6.10}
Remote Desktop - User Mode (UDP-In)       {10.10.5.0/255.255.255.0, 10.10.6.10}

From a client, connect with Restricted Admin mode (credentials never sent to the server; you need to be in the server's local Administrators group) or with Remote Credential Guard (Kerberos only, direct connections only):

powershell
mstsc.exe /v:<server> /restrictedAdmin
mstsc.exe /v:<server> /remoteGuard

Script

powershell
<#
.SYNOPSIS
    Hardens Windows Server RDP: NLA, TLS, session limits, redirection, and a source-IP allowlist.
.DESCRIPTION
    On the RDP-Tcp listener, requires Network Level Authentication
    (UserAuthentication = 1), the TLS security layer (SecurityLayer = 2) and High
    encryption (MinEncryptionLevel = 3). Writes idle and disconnected session
    limits and optional drive and clipboard redirection blocks to the Terminal
    Services policy key. Optionally allows Restricted Admin mode and Remote
    Credential Guard, and disables TLS 1.0/1.1 server protocols. Scopes the
    enabled inbound rules in the Remote Desktop firewall group to the allowlist,
    warns about other rules that open TCP 3389, and warns when Group Policy
    already controls a listener setting.
.PARAMETER AllowedSource
    IP addresses or CIDR ranges allowed to reach RDP. Must include the range you
    are connecting from, or you will lock yourself out.
.PARAMETER IdleTimeoutMinutes
    Idle time before an active session is disconnected. Default 30.
.PARAMETER DisconnectedSessionTimeoutMinutes
    How long a disconnected session is kept before it is ended. Default 60.
.PARAMETER DisableDriveRedirection
    Blocks client drive mapping (and clipboard file copy) into sessions.
.PARAMETER DisableClipboardRedirection
    Blocks clipboard sharing between the client and the session.
.PARAMETER AllowRestrictedAdmin
    Sets DisableRestrictedAdmin = 0 so clients can use Restricted Admin mode or
    Remote Credential Guard.
.PARAMETER DisableLegacyTls
    Disables the TLS 1.0 and TLS 1.1 server protocols in Schannel. Restart required.
.EXAMPLE
    .\Set-HardenedRdpConfiguration.ps1 -AllowedSource "10.10.5.0/24" -IdleTimeoutMinutes 15 -DisableDriveRedirection
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2025-09-17)
    Requires: Windows Server 2016 or later, NetSecurity module, Administrator
#>
[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)]
    [string[]]$AllowedSource,

    [ValidateRange(1, 1440)]
    [int]$IdleTimeoutMinutes = 30,

    [ValidateRange(1, 1440)]
    [int]$DisconnectedSessionTimeoutMinutes = 60,

    [switch]$DisableDriveRedirection,

    [switch]$DisableClipboardRedirection,

    [switch]$AllowRestrictedAdmin,

    [switch]$DisableLegacyTls
)

$listenerPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
$policyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services"
$lsaPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa"

if (-not (Test-Path -Path $listenerPath)) {
    throw "RDP-Tcp listener key not found. Is Remote Desktop configured on this host?"
}

# Warn when Group Policy already sets a listener value; the GPO wins over the local registry.
$general = Get-CimInstance -Namespace "root\cimv2\TerminalServices" -ClassName Win32_TSGeneralSetting -Filter "TerminalName='RDP-Tcp'" -ErrorAction SilentlyContinue

if ($general) {
    foreach ($source in @("PolicySourceUserAuthenticationRequired", "PolicySourceSecurityLayer", "PolicySourceMinEncryptionLevel")) {
        if ($general.$source -eq 1) {
            Write-Warning "$source = Group Policy. The local value set below will be overridden; change the GPO instead."
        }
    }
}

# Listener: NLA required, TLS security layer, High encryption.
Set-ItemProperty -Path $listenerPath -Name "UserAuthentication" -Value 1 -Type DWord
Set-ItemProperty -Path $listenerPath -Name "SecurityLayer" -Value 2 -Type DWord
Set-ItemProperty -Path $listenerPath -Name "MinEncryptionLevel" -Value 3 -Type DWord

# Session limits and redirection through the policy key. Values are milliseconds.
if (-not (Test-Path -Path $policyPath)) {
    New-Item -Path $policyPath -Force | Out-Null
}

New-ItemProperty -Path $policyPath -Name "MaxIdleTime" -Value ($IdleTimeoutMinutes * 60000) -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $policyPath -Name "MaxDisconnectionTime" -Value ($DisconnectedSessionTimeoutMinutes * 60000) -PropertyType DWord -Force | Out-Null

if ($DisableDriveRedirection) {
    New-ItemProperty -Path $policyPath -Name "fDisableCdm" -Value 1 -PropertyType DWord -Force | Out-Null
}

if ($DisableClipboardRedirection) {
    New-ItemProperty -Path $policyPath -Name "fDisableClip" -Value 1 -PropertyType DWord -Force | Out-Null
}

# Restricted Admin mode and Remote Credential Guard need delegation of non-exportable credentials.
if ($AllowRestrictedAdmin) {
    New-ItemProperty -Path $lsaPath -Name "DisableRestrictedAdmin" -Value 0 -PropertyType DWord -Force | Out-Null
}

# Legacy TLS off for every Schannel server on the box. Takes effect after a restart.
if ($DisableLegacyTls) {
    foreach ($protocol in @("TLS 1.0", "TLS 1.1")) {
        $serverKey = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$protocol\Server"

        if (-not (Test-Path -Path $serverKey)) {
            New-Item -Path $serverKey -Force | Out-Null
        }

        New-ItemProperty -Path $serverKey -Name "Enabled" -Value 0 -PropertyType DWord -Force | Out-Null
    }

    Write-Warning "TLS 1.0/1.1 server protocols disabled. Restart the server for Schannel to pick this up."
}

# Firewall: scope the enabled inbound Remote Desktop rules to the allowlist.
$rdpRules = @(Get-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue |
    Where-Object { $_.Direction -eq "Inbound" -and $_.Enabled -eq "True" })

if ($rdpRules.Count -eq 0) {
    Write-Warning "No enabled inbound Remote Desktop firewall rules found. Firewall scope was not changed."
}

foreach ($rule in $rdpRules) {
    Set-NetFirewallRule -Name $rule.Name -RemoteAddress $AllowedSource
}

# Any other enabled allow rule on TCP 3389 would bypass the allowlist.
$rdpRuleNames = $rdpRules | ForEach-Object { $_.Name }
$otherRules = Get-NetFirewallPortFilter -Protocol TCP |
    Where-Object { $_.LocalPort -contains "3389" } |
    Get-NetFirewallRule |
    Where-Object { $_.Enabled -eq "True" -and $_.Direction -eq "Inbound" -and $_.Action -eq "Allow" -and $rdpRuleNames -notcontains $_.Name }

foreach ($other in $otherRules) {
    $remote = ($other | Get-NetFirewallAddressFilter).RemoteAddress -join ", "
    Write-Warning "Rule '$($other.DisplayName)' (enabled, inbound, TCP 3389) is not in the Remote Desktop group and still allows RemoteAddress $remote. Review it; this script does not change it."
}

# Report the resulting configuration.
$listener = Get-ItemProperty -Path $listenerPath
$lsa = Get-ItemProperty -Path $lsaPath

[PSCustomObject]@{
    UserAuthentication     = $listener.UserAuthentication
    SecurityLayer          = $listener.SecurityLayer
    MinEncryptionLevel     = $listener.MinEncryptionLevel
    DisableRestrictedAdmin = $lsa.DisableRestrictedAdmin
} | Format-Table -AutoSize

Get-ItemProperty -Path $policyPath |
    Select-Object -Property MaxIdleTime, MaxDisconnectionTime, fDisableCdm, fDisableClip |
    Format-Table -AutoSize

$rdpRules |
    ForEach-Object {
        [PSCustomObject]@{
            RuleName      = $_.DisplayName
            RemoteAddress = (Get-NetFirewallRule -Name $_.Name | Get-NetFirewallAddressFilter).RemoteAddress
        }
    } |
    Format-Table -AutoSize

Notes

  • Order of trust. Run this from a session already inside AllowedSource, and keep a console or out-of-band connection open the first time, in case the allowlist is wrong.
  • SecurityLayer, not MinEncryptionLevel, is the one that matters. SecurityLayer is 0 (native RDP security), 1 (Negotiate, the default) or 2 (TLS). Microsoft's policy text for "Require use of specific security layer for remote (RDP) connections" calls TLS "the recommended setting," and notes that with RDP or a Negotiate fallback "the RD Session Host server isn't authenticated." The "Set client connection encryption level" setting (MinEncryptionLevel: 1 Low, 2 Client Compatible, 3 High, 4 FIPS) "only applies when you are using native RDP encryption" and "doesn't apply to SSL encryption." The script sets it to High anyway so a later change back to Negotiate doesn't silently fall to Low.
  • The certificate. With SecurityLayer = 2 and nothing else configured, the listener uses its self-signed certificate, so clients see a certificate warning unless you deploy a CA-issued one. The Win32_TSGeneralSetting WMI class exposes SSLCertificateSHA1Hash (read/write) for pointing the listener at a certificate by thumbprint.
  • Group Policy wins. Values under HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services are where the Remote Desktop Session Host policies land (for example UserAuthentication, fDisableCdm, fDisableClip, and MaxIdleTime in milliseconds). Writing them locally works on a standalone server, but a domain GPO that configures the same setting overwrites them at the next refresh. For a fleet, set the same values in a GPO under Computer Configuration > Administrative Templates > Windows Components > Remote Desktop Services > Remote Desktop Session Host. Session limit changes apply to new sessions; existing sessions keep the old limit until they reconnect.
  • Restricted Admin vs Remote Credential Guard. Both keep credentials off the target and prevent pass-the-hash, but they differ: Restricted Admin connects to other resources as the server's identity and requires local Administrators membership; Remote Credential Guard gives single sign-on onward, needs Kerberos (no NTLM fallback), and doesn't work through RD Gateway or Connection Broker. Microsoft recommends Restricted Admin for helpdesk connections to possibly compromised machines. To force clients to use one or the other, set "Restrict delegation of credentials to remote servers" under Computer Configuration > Administrative Templates > System > Credentials Delegation on the clients.
  • Account lockout. Brute-force protection is the lockout policy, not RDP. Since the October 11, 2022 cumulative updates, Windows supports locking out the built-in local Administrator, and Microsoft's recommended baseline is 10 failed attempts within 10 minutes, locked for 10 minutes. It's on by default only for new Windows 11 22H2 installs; existing servers need it set. On domain members, lockout for domain accounts comes from the domain password policy. For local accounts on a standalone box: net accounts /lockoutthreshold:10 /lockoutwindow:10 /lockoutduration:10.
  • Legacy TLS. -DisableLegacyTls changes Schannel for the whole server, not just RDP: IIS, SQL Server connections, LDAPS on a DC and anything else using Schannel lose TLS 1.0 and 1.1 after the restart. Microsoft is disabling both by default in new Windows releases, starting with the 2024 Windows 11 and Windows Server Insider builds, and documents event 36871 as the sign of an application that fails once they're off.
  • The firewall lookup uses the English display group name "Remote Desktop". On a localized OS, find the group name with Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP" | Select-Object -Property DisplayGroup and adjust.
  • This doesn't replace putting RDP behind a VPN or an RD Gateway. The allowlist narrows who can reach the port; it adds no authentication beyond NLA.

Source