If you enable RDP on a Windows Server and expose port 3389 to the internet, you'll see a brute-force attempt within an hour. Within a day, hundreds. Within a week, your event log is unreadable.
The fix: enforce NLA, restrict by IP at the firewall level, audit what's left. Five PowerShell commands, half a minute. Do them on every server with internet-reachable RDP.
TL;DR:
# Enforce NLA
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name 'UserAuthentication' -Value 1
# Restrict the RDP firewall rule to a specific IP/range
Set-NetFirewallRule -DisplayGroup "Remote Desktop" -RemoteAddress 203.0.113.5
# Audit failed logons
auditpol /set /subcategory:"Logon" /failure:enable /success:enable
That's the spine. Details and the rare exceptions below.
NLA — what it is and why it matters
Network Level Authentication. RDP without NLA accepts the connection first, then asks for credentials over the established session. Brute-force tools love this — they can churn passwords without the OS counting failures, and the early connection bypass leaks server fingerprints.
NLA flips the order: client must authenticate before the server allocates session resources. Brute-force still possible, but slower and more visible.
# Check current state
(Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp').UserAuthentication
# 1 = NLA required, 0 = legacy mode
Enable:
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name 'UserAuthentication' -Value 1
No reboot needed. Existing sessions stay; new connections require NLA.
The exception: very old RDP clients (Windows XP, Server 2003) can't do NLA. By 2026, this is irrelevant — those OSes are 20 years EOL. Don't keep NLA off for compatibility.
Firewall — the actual hardening
NLA slows brute force. An IP allowlist stops it dead. If the only IPs that can reach 3389 are your office and your home, the bot net is talking to a closed port.
# See current scope
Get-NetFirewallRule -DisplayGroup "Remote Desktop" | Get-NetFirewallAddressFilter
# Restrict to one IP
Set-NetFirewallRule -DisplayGroup "Remote Desktop" -RemoteAddress 203.0.113.5
# Restrict to a range or multiple
Set-NetFirewallRule -DisplayGroup "Remote Desktop" -RemoteAddress @(
'203.0.113.5', # office
'198.51.100.0/24', # home block
'10.0.0.0/8' # internal LAN
)
# Lock to LAN only (no internet RDP at all)
Set-NetFirewallRule -DisplayGroup "Remote Desktop" -RemoteAddress LocalSubnet
LocalSubnet is the simplest and the strongest. If you can VPN to the LAN (WireGuard works fine), do RDP over the VPN — it's not internet-exposed at all.
Verifying the firewall change
Get-NetFirewallRule -DisplayGroup "Remote Desktop" |
Get-NetFirewallAddressFilter |
Select-Object DisplayName, RemoteAddress
The remote-address column should show your allowlist, not Any.
Test from outside the allowlist:
nc -zv your.server.ip 3389
Should fail. From inside (or after VPN), should succeed. If both succeed, the firewall rule didn't apply — re-check.
Audit failed logons
NLA and firewall together stop most attacks. The few that get through (or insiders) need to be visible. Enable audit:
auditpol /set /subcategory:"Logon" /failure:enable /success:enable
auditpol /set /subcategory:"Account Lockout" /failure:enable
Failed RDP logons land in Security event log as Event ID 4625. Filter:
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625} -MaxEvents 50 |
Select-Object TimeCreated,
@{n='User';e={$_.Properties[5].Value}},
@{n='Source';e={$_.Properties[19].Value}}
Tells you who tried to log on, from where. If you've locked the firewall properly, this list should be empty for external IPs. If it isn't, your firewall rule isn't doing what you think.
Account lockout policy
Even with NLA + firewall, bound the damage of a successful guess by locking accounts after N failures:
net accounts /lockoutthreshold:5
net accounts /lockoutduration:30
net accounts /lockoutwindow:30
5 failed attempts in 30 minutes → 30-minute lockout. Standard balance — annoying enough to slow attackers, not so harsh that fat-fingering your own password locks you out for hours.
For domain accounts, push this via GPO:
Computer Configuration → Policies → Windows Settings → Security Settings →
Account Policies → Account Lockout Policy
If you're not on a domain yet, see joining a server to AD.
Disable RDP for the local Administrator account
A common attack pattern: bot guesses Administrator plus common passwords, eventually hits. Even on a fresh box, "Administrator" is the obvious user — don't make it the easy target.
Either rename it:
Rename-LocalUser -Name "Administrator" -NewName "svc-admin-lukas"
Or, better, create a separate admin user (per creating Windows users from cmd) and disable the built-in:
net user backup-admin StrongPass123! /add
net localgroup Administrators backup-admin /add
net user Administrator /active:no
Now Administrator doesn't exist as a login target.
Change the RDP port — does it help?
Moving RDP from 3389 to 13389 cuts the noise dramatically. Bots scan 3389 by default; few scan all ports. But:
- It's security through obscurity — a determined attacker scans regardless.
- It breaks tooling that assumes 3389.
- Audit logs still fill up if you don't ALSO restrict by IP.
The IP allowlist is the actual fix. Port change is optional cleanup. If you want to do both:
$port = 13389
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
-Name 'PortNumber' -Value $port
New-NetFirewallRule -DisplayName "RDP-Custom" -Direction Inbound `
-Action Allow -Protocol TCP -LocalPort $port -RemoteAddress 203.0.113.5
Set-NetFirewallRule -DisplayGroup "Remote Desktop" -Enabled False
Restart-Service TermService -Force
Connect with mstsc /v:server.example.com:13389. Don't forget — if you do, the recovery option is the Utilman trick or going via console.
What this combines with
- BitLocker on Windows Server — physical-access protection. RDP hardening + BitLocker covers both vectors.
- SMBv1, LLMNR, NTLMv1 disable — protocol hardening for everything else.
- Windows Server initial setup checklist — the broader baseline this all fits inside.
The actually-better answer: don't expose RDP at all
If you have any choice, don't put 3389 on the internet. Run WireGuard on the server (or on a separate jumpbox), connect to the VPN first, then RDP over the tunnel. The attack surface drops from "global internet" to "your VPN endpoint", which you can lock down to a few keys.
Same operational comfort, fundamentally smaller exposure. The hardening above is for when you can't do the VPN-only setup. If you can, do that.
Gotchas
- Editing the RDP firewall rule via the GUI vs PowerShell.
Set-NetFirewallRule -DisplayGroup "Remote Desktop"modifies all rules in the group (TCP + UDP, inbound + the WS-Management variants). The GUI lets you edit one, miss the others, and end up with inconsistent scope. Use PowerShell. - Office IP changes. ISPs rotate. If your home/office uses a dynamic IP and you allowlist it, you'll lock yourself out one Tuesday. Either get a static IP, set up a DDNS allowlist (more work), or VPN.
- NLA breaks some clients on first cert mismatch. If the server's TLS cert is self-signed, some RDP clients prompt or refuse. Replace with an internal CA cert if your org has one.
- Forgetting to test from outside. "I changed the firewall, hopefully it works" is how you discover Tuesday morning that bots are still hammering. Test from a phone hotspot or a remote VPS that isn't in your allowlist.
For the basic enable side (before hardening), see enabling RDP from PowerShell or cmd.