Battle-tested Windows Server VPS hardening script. Born from a real RDP brute-force compromise, the recovery, and the lessons.
A PowerShell script that takes a fresh Windows Server VPS from "default-and-vulnerable" to "hardened" in about 15 minutes. Single file, no dependencies, opinionated defaults, safety nets so you don't lock yourself out.
Tested on Windows Server 2016, 2019, 2022, and 2025.
I bought a Contabo VPS for an automated trading setup. Within 3 hours of going online, the RDP service was being brute-forced by botnets — 400+ failed login attempts in the first hour, from notorious IP ranges (3.65.40.162, 45.133.195.x, 181.215.65.x).
The server got slow. CPU pegged at 50%+. I thought it was my trading software. It wasn't — it was TermService (the RDP service) consuming 2,400+ CPU seconds just handling brute-force authentication attempts. By the time I noticed, an attacker had an established TCP connection on port 3389 and was actively trying credentials.
I closed off RDP, audited the server, and decided to reinstall rather than try to clean up — you can never prove a Windows Server is clean once attackers had an established connection.
Then I rebuilt it properly. This script is the result.
A single PowerShell script that performs every step a hardening guide tells you to do, in the right order, with safety checks between destructive actions.
- Create a new admin account with a strong password (you specify the name)
- Disable the default
Administratoraccount (only after verifying replacement works) - Disable
Guest,DefaultAccount, cloud-provider provisioning accounts - Set account lockout policy (5 failed attempts → 30 min lockout)
- Enforce 14+ character passwords with complexity requirements
- Detect your current public IP and restrict RDP to it
- Move RDP off the default port
3389to a randomized high port - Verify Network Level Authentication (NLA) is enabled
- Add public-IP fallback rule before disabling broader access (the safety net pattern)
- Force
TermServicestartup toAutomatic(defaultManualcauses lockouts after reboot)
- Disable rules for protocols you don't need:
mDNS,Cast to Device,AllJoyn,Microsoft Edge networking,Wireless Display,Media Foundation,Delivery Optimization - Disable
WinRM(service + firewall + listener +LocalAccountTokenFilterPolicyreset) - Disable OpenSSH if not in use
- Block known brute-force IP ranges with explicit deny rules
- Xbox services, DiagTrack telemetry, MapsBroker, Windows Search indexer
- Fax, PrintNotify, Tablet Input, biometric services
- Optional aggressive trim for telemetry workers and scheduled diagnostic tasks
- Path and process exclusions for your workload (configurable)
- Schedule full scans for off-hours
- Lower CPU priority during scans
- Disable archive/network-drive scanning (irrelevant on a VPS)
- Disable telemetry uploads (
MAPSReporting, sample submission)
- Optional installation of IPBan — auto-bans IPs after N failed authentication attempts
- Optional Tailscale install with
--unattendedflag (the configuration flag everyone forgets, leading to lockout after reboot) - DNSSEC reminders if your domain registrar supports it
On the fresh Windows Server VPS, in an elevated PowerShell session:
# Download and run
iwr -useb https://raw.githubusercontent.com/kastlevps/bastion-script/main/harden.ps1 | iexOr download the script first, review it, then run it (recommended on a server you care about):
iwr -useb https://raw.githubusercontent.com/kastlevps/bastion-script/main/harden.ps1 -OutFile harden.ps1
notepad harden.ps1 # review it
.\harden.ps1The script is interactive by design — it asks for your public IP, the new admin username, and confirmation before each destructive section.
| Prompt | Example | Why |
|---|---|---|
| Your public IP | 203.0.113.45 |
RDP allow-list. Auto-detected via ifconfig.me if you press Enter |
| New admin username | myadmin |
Avoid using Administrator (most-attacked username globally) |
| New admin password | (hidden) | 14+ chars enforced. Save it in a password manager BEFORE pasting |
| New RDP port | 47823 |
Random unprivileged port. Default suggested if you press Enter |
| Workload type | trading / web / general |
Tunes Defender exclusions and disabled services |
The script encodes lessons learned the hard way. The biggest one:
Never disable a working access path until the replacement has survived a real reboot test.
Specifically:
- Add the new firewall rule first, then narrow the existing one — never replace atomically.
- Test login as the new admin before disabling
Administrator. - Verify Tailscale auto-reconnects after reboot (
tailscale up --unattendedis the missing piece) before removing public RDP. - Always keep at least one non-VPN access path (public IP allow-list) as a fallback — VNC alone is not enough because cloud-provider VNC sessions time out and credentials regenerate per session.
- Service startup type matters:
TermServicedefaults toManualon some Windows builds, which means RDP doesn't auto-start after reboot. Force it toAutomatic.
These lessons came from a 4-hour lockout incident during the original hardening work. The script gates every destructive change on a verification step so you can't repeat them.
By design, this is a bootstrap hardening script, not a continuous monitoring solution. It runs once and exits. After it runs, you should:
- Snapshot the VPS via your provider (Contabo, Hetzner, AWS, etc.) as your "clean baseline"
- Set up your actual workload (web app, trading platform, game server, etc.)
- Snapshot again as "ready to deploy"
- Periodically review failed logon counts in Event Viewer (or use the included
health-check.ps1)
For continuous audits, real-time alerts, weekly reports, and provider-integrated snapshot management, see KastleVPS (which I'm building based on what this script taught me).
Yes — but take a snapshot first. The script is non-destructive of user data; it only changes service states, firewall rules, and account configurations. But any sufficiently complex hardening can have unintended consequences. Snapshot first.
The Defender exclusions are configurable. The script will show you what it's about to add and ask for confirmation. Most trading platforms (MT4/MT5, NinjaTrader, cTrader) and common web stacks are pre-recognized.
Mostly — it's tuned for Server SKUs, but the security parts (RDP hardening, firewall lockdown, Defender tuning) work fine on desktop Windows. Some service-trim items don't exist on desktop and will be silently skipped.
The script always preserves at least one access path (public IP allow-list). Even if Tailscale fails, you can RDP from your home IP. If everything fails, your cloud provider's VNC console (Contabo, Hetzner, AWS Console Connect, etc.) bypasses the firewall completely. See the recovery notes at the end of the script's output.
Not yet. Code signing requires a Microsoft Authenticode certificate ($300+/year). For now, the script is plain text — review it yourself before running. PRs welcome.
So it works on every Windows Server release from 2016 onward without requiring users to install PowerShell 7. Server 2025's built-in PowerShell can run this without modification.
This script is a one-shot, interactive hardening tool. If you want:
- Continuous monitoring of failed logons and account drift
- Weekly audit reports emailed to you
- Real-time alerts when a brute-force spike or new admin account appears
- Provider-integrated snapshots before each major change
- Workload-specific profiles (trading server, game server, etc.) with pre-tuned exclusions
- Recovery wizards for every major VPS provider (Contabo, Hetzner, AWS, etc.)
→ KastleVPS is building exactly that. Drop your email there to get notified at launch.
PRs welcome. Especially:
- Hardening checks for OS versions I haven't tested (Server Core, Nano Server, ARM64)
- Provider-specific cleanups (cloud-init accounts beyond
cloudbase-*) - Localization (the script currently assumes US-English locale strings)
- Translations of this README
Open an issue first for big changes so we can align on scope.
MIT — do whatever you want with it. Attribution appreciated but not required.
If this saved you from a compromise, tell me about it. Stories help me make the script better.
This script exists because:
- The bots at
3.65.40.162and friends taught me to take RDP seriously - The
TermServicerunning for 4 hours straight at 100% CPU got my attention - A lockout incident during the rebuild taught me about the safety-net pattern
- IPBan and Tailscale do the heavy lifting at the edge
If you got hit by a similar attack and recovered, share your story — these accounts make the public hardening guides better for everyone.