How to protect a Windows VPS from RDP brute force attacks
Harden a Windows Server VPS against RDP brute force attacks using firewall rules, port change, lockout policy and automatic IP blocking.
Every Windows VPS exposed to the public internet starts receiving RDP login attempts within minutes of boot. Bots scan entire IP ranges looking for port 3389 open and fire dictionaries of common passwords at accounts like Administrator, admin, user and variations. On an unprotected VPS the Event Log fills with Event ID 4625 (logon failure) in a few hours — and a single weak password is all it takes for an attacker to get in.
This tutorial is for Windows sysadmins who recently provisioned a VPS and want to reduce the RDP attack surface before putting the server into production. The measures here don’t require third-party software — they only use native features of Windows Server 2019/2022/2025. Estimated time: 30 to 45 minutes to apply all layers.
The approach is layered — no single measure is enough on its own. You’ll apply four defenses: an account lockout policy, an RDP port change, firewall restriction by source IP and automatic blocking of offending IPs via an event-triggered Scheduled Task.
Prerequisites
Windows Server 2019, 2022 or 2025 VPS with administrative access. A working RDP connection. Know your office/home public IP before you start — if you restrict RDP and lose that IP, you’ll need the VPS console to recover access.
TCP 3389 4625 TermService Before starting, open Event Viewer at Windows Logs > Security and filter by Event ID 4625. On a VPS that’s been exposed for more than 24 hours, you’ll typically see hundreds of attempts coming from IPs in China, Russia, Brazil and the US. That’s the baseline we’re going to crush.
Apply an account lockout policy
The lockout policy makes Windows temporarily disable an account after N failed attempts. Without it, an attacker can fire thousands of passwords per minute at the Administrator account. With a 5-attempt lockout and a 30-minute block, the attacker can test at most 240 passwords per day per account — making dictionary attacks unfeasible.
Open the Local Group Policy Editor (gpedit.msc) and navigate to:
Computer Configuration > Windows Settings > Security Settings >
Account Policies > Account Lockout PolicyConfigure the three values:
- Account lockout threshold: 5 invalid attempts
- Account lockout duration: 30 minutes
- Reset account lockout counter after: 30 minutes
Apply the policy immediately without waiting for the 90-minute Group Policy cycle:
gpupdate /forceVerify it’s active:
net accountsYou should see Lockout threshold: 5 and Lockout duration (minutes): 30 in the output.
Setting lockout duration to 0 (permanent) looks safer but creates operational risk: any typo on your end locks the account until manual intervention. 30 minutes is the standard balance recommended by both Microsoft and the CIS Benchmark.
Change the default RDP port
Switching from 3389 to a high port (above 49152, the dynamic range) eliminates the noise from automated scanners that only look for the default port. It’s not real security against a targeted attack — nmap -p- IP still finds it — but it drops Event ID 4625 by 90%+ on exposed VPSes.
Open the Registry Editor (regedit) and navigate to:
HKLM\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-TcpFind the PortNumber key, switch the view to decimal and enter the new port (example: 54289). Avoid ports known to other services (8080, 8443, 3306).
Add the firewall rule for the new port before restarting the service:
New-NetFirewallRule -DisplayName "RDP Custom Port 54289 TCP" `
-Direction Inbound -LocalPort 54289 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "RDP Custom Port 54289 UDP" `
-Direction Inbound -LocalPort 54289 -Protocol UDP -Action AllowRestart the Terminal Server service to apply the change:
Restart-Service TermService -ForceYour current session will drop. Reconnect using IP:54289 in the RDP client (Windows’ Remote Desktop Connection accepts the :port suffix).
If you restart TermService without first creating the firewall rule for the new port, RDP becomes unreachable. On a Hostini VPS you can get back in via the console in the panel — but to avoid that pain, make sure both rules are in place before Restart-Service.
Restrict RDP by source IP
The most effective measure: the firewall only accepts RDP connections from specific IPs. Works well if your team accesses from fixed IPs (office, corporate VPN). It doesn’t work if you need to access from residential networks with dynamic IPs — in that case, consider the WireGuard tunnel alternative.
Identify the IPs that should be allowed. Check your current public IP:
(Invoke-WebRequest -Uri "https://ifconfig.me/ip").ContentWrite down that IP and any other location that needs access.
Edit the firewall rule you created in the previous section to accept only those IPs:
Set-NetFirewallRule -DisplayName "RDP Custom Port 54289 TCP" `
-RemoteAddress @("203.0.113.10", "198.51.100.42")Replace the example IPs with yours. For a CIDR range use the format 203.0.113.0/24.
Confirm the rule is restricted:
Get-NetFirewallRule -DisplayName "RDP Custom Port 54289 TCP" |
Get-NetFirewallAddressFilterThe RemoteAddress field should show the IPs you defined, not Any.
If you connect from a residential ISP with a dynamic IP, instead of constantly updating the allowlist, install a WireGuard server on the VPS itself, expose only UDP 51820 publicly and restrict RDP to 10.0.0.0/24 (the tunnel’s internal network). You connect via VPN first, then RDP.
Automatically block offending IPs
Even with lockout and a changed port, if you can’t restrict by source IP (public-facing service, distributed team), configure dynamic blocking: every time Event ID 4625 appears above a threshold, a Scheduled Task fires a PowerShell script that adds the source IP to a blocking firewall rule.
Save the script below to C:\Scripts\BlockBruteforceIP.ps1:
param([string]$EventRecordID)
$event = Get-WinEvent -LogName Security -FilterXPath `
"*[System[EventRecordID=$EventRecordID]]"
$ip = ($event.Properties | Where-Object { $_.Value -match '^\d+\.\d+\.\d+\.\d+$' } |
Select-Object -First 1).Value
if (-not $ip -or $ip -eq "127.0.0.1") { exit }
$ruleName = "AutoBlock-$ip"
if (-not (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound `
-RemoteAddress $ip -Action Block | Out-Null
Add-Content "C:\Scripts\blocked-ips.log" "$(Get-Date -Format o) BLOCKED $ip"
}Create the event-triggered Scheduled Task. Open Task Scheduler > Create Task:
- General > Run with highest privileges: checked
- Triggers > New: Begin the task =
On an event, Log =Security, Event ID =4625 - Actions > New: Program =
powershell.exe, Arguments:
-ExecutionPolicy Bypass -File C:\Scripts\BlockBruteforceIP.ps1 -EventRecordID $(EventRecordID)To pass $(EventRecordID) correctly, you need to edit the task XML after creation — export the task, edit the <ValueQueries> element to add the RecordId, and re-import.
Test by forcing a failed login from another machine and confirm the IP was blocked:
Get-NetFirewallRule | Where-Object DisplayName -like "AutoBlock-*"You can also review the C:\Scripts\blocked-ips.log file to see the history.
If setting up the Scheduled Task by hand feels fragile, the open-source IPBan project does exactly this robustly and for free. It monitors Event Logs in real time and maintains optimized firewall rules. For larger environments it’s worth it.
Verification
After applying the four layers, confirm each one is in place by running:
# Lockout policy
net accounts | findstr "Lockout"
# New RDP port active
Get-ItemProperty "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" `
| Select-Object PortNumber
# Firewall accepts connection on the new port
Test-NetConnection -ComputerName localhost -Port 54289
# Account actually hardened — try a bad login 5x from another IP
# and look for Event ID 4740 (Account Locked Out) in the Event Log
Get-WinEvent -LogName Security -MaxEvents 50 |
Where-Object Id -eq 4740 | Format-Table TimeCreated, Message -AutoSize
Wait 24 hours and revisit Event Viewer. You should see Event ID 4625 drop from hundreds/thousands per day to dozens or zero, depending on how exposed the original port was before the change.
Next steps
- Enable NLA (Network Level Authentication) if it isn’t on — it requires authentication before the full RDP handshake, shrinking the attack surface to pre-auth vulnerabilities.
- Consider moving off native RDP to Azure Bastion or an RDS gateway with MFA if the environment justifies it.
- Set up successful logon auditing (Event ID 4624) and email alerts for logins outside business hours.
- Take periodic backups of the registry (
HKLM\System\CurrentControlSet\Control\Terminal Server) — so you can revert quickly if something breaks. - If you’re provisioning a new VPS for production, a Hostini VPS Windows already ships with out-of-band console access through the panel — so even if you lock yourself out of RDP while applying these rules, recovery doesn’t require a support ticket.
Frequently asked questions
Does changing the RDP port from 3389 to something else actually help?
Yes, but as noise reduction, not as real security. Automated scanners that sweep the entire internet target 3389 and stop finding you when you change it. Targeted attackers still find you via nmap. Combine it with a lockout policy and IP blocking — an alternate port alone is security by obscurity.
Why isn't Windows Defender Firewall enough on its own?
The default firewall allows RDP from any source as soon as you enable Remote Desktop. It has no native brute force detection — it allows or blocks based on static rules. You need to add source rules (IP allowlist) or a dynamic blocking mechanism driven by Event Viewer.
Is it safe to close port 3389 and only use an SSH or WireGuard tunnel?
It's the most secure approach. You expose only the tunnel port (UDP 51820 for WireGuard, for example) and RDP only accepts connections from 127.0.0.1 or the tunnel's internal network. A public internet attacker can't even complete a TCP handshake with RDP. Trade-off: you have to configure the client on every machine you'll administer from.
Won't the lockout policy lock out the administrator?
It can if you mistype your password during a real session. That's why the recommendation is 15-30 minute lockouts, not permanent, and keeping a second administrative account with a different name for recovery. On a Hostini VPS you still have console access through the panel even if RDP locks the main account.
Does Event ID 4625 catch every failed RDP attempt?
It catches the ones that reach the authentication layer, which is the case when NLA (Network Level Authentication) is enabled and the client sends credentials. Attempts that fail before the handshake completes (probe scans) don't generate 4625 — they show up in the Security Log as Event 4624 (logon) or in the TerminalServices logs. For full coverage, combine 4625 with Microsoft-Windows-RemoteDesktopServices/Operational.
Is it worth using fail2ban on Windows?
Fail2ban is Linux native. On Windows the functional equivalent is a Scheduled Task triggered by Event ID 4625 plus a PowerShell script that adds the offending IP to a blocking firewall rule. Open-source tools wrap this for you (RDPGuard, IPBan, EvlWatcher) — IPBan is free and the closest to a fail2ban-like experience.