Securing RDP on a Windows VPS: Stop Exposing Port 3389
A Windows server with Remote Desktop open to the internet is scanned and attacked continuously — not targeted, just found by software. And when Windows runs inside a VM on your VPS, the port is published by the Linux host, not by Windows. Here is where it actually lives, and how to close it.
01Why Port 3389 Gets Found
Remote Desktop Protocol listens on TCP port 3389. The moment a machine answers on that port from a public IP address, it joins a list that is scraped and re-scraped constantly. Nobody chose your server. Software walked the whole IPv4 range, found an open port, and wrote down the address.
If you have run a Linux server before, you already know the feeling of watching SSH authentication failures pile up in the logs. RDP attracts the same attention, and it is a softer target for one simple reason: there is a well-known administrator account name to guess, and password authentication is switched on by default.
A checklist that begins with “enable Network Level Authentication” is treating the symptom. The real problem is that the port is reachable from the entire internet, and no set of Windows settings changes that.
This is worth saying plainly, because a lot of guides do not: if you installed Windows with OS Installer, the script opens port 3389 for you. That is deliberate. You cannot connect to a Windows desktop that has no way in, and opening the port is what turns a freshly installed VM into something you can actually use. It is the right default to get started — and it is the first thing worth revisiting once you are in.
02Your Port Is Opened by Linux, Not Windows
This is the part that almost every RDP hardening guide gets wrong for a setup like this one, and it is worth understanding before you change anything.
Windows is not running on the bare metal of your server. It is a virtual machine, started by QEMU on the Ubuntu host. QEMU hands it a virtual network card and forwards traffic from the host to the guest, and that forwarding is what publishes the port. The line responsible lives on the Linux side, in the start script the installer wrote for you:
-netdev user,id=net0,hostfwd=tcp::3389-:3389
Read that argument closely. tcp::3389 has an empty address field
before the port, which in QEMU’s syntax means “listen on every interface” —
the same thing as 0.0.0.0:3389. The host then accepts that traffic and passes it
through to the Windows guest, which knows nothing about any of it.
The installer also adds a matching rule to the Linux firewall, because a forwarded port that the firewall drops is a port that does nothing:
iptables -I INPUT -p tcp --dport 3389 -j ACCEPT
So there are two layers, and they do different jobs. On the host, Linux decides whether the port is published at all. Inside the guest, Windows decides who is allowed to authenticate. Almost every guide on the internet was written for a machine where those two layers are the same computer, and their advice quietly assumes it.
| Layer | What it controls | Where you change it |
|---|---|---|
| Ubuntu host | Whether 3389 is reachable from the internet at all | SSH, /usr/local/bin/osip-qemu-start.sh, iptables |
| Windows guest | Who is allowed to log in once a connection arrives | RDP, inside the VM |
If you only harden Windows, the port stays published and keeps collecting attention — you have improved the lock without removing the door from the street. If you only close the host port without setting up a way in, you lock yourself out. Sections 04 and 07 deal with each layer, in that order.
03Check How Exposed You Are
Two measurements tell you almost everything: whether the port answers from outside, and how much noise is already arriving. There is also a quick check on the host itself.
ss -ltnp | grep 3389
What you want to see is an address other than 0.0.0.0. If it prints
0.0.0.0:3389, the port is published on every interface your server has. If it
prints 127.0.0.1:3389, only the server itself can reach it — which is the
state section 04 moves you to.
nc -vz YOUR_SERVER_IP 3389
If it reports succeeded or open, your desktop is reachable by anyone. On
Windows you can use Test-NetConnection YOUR_SERVER_IP -Port 3389 instead. Run
this from home, not from the server, or you will just be testing the loopback and get a
misleading answer.
Get-WinEvent -FilterHashtable @{LogName='Security';Id=4625} |
Measure-Object | Select-Object -ExpandProperty Count
Event ID 4625 is the “an account failed to log on” record. On a machine that has been exposed for a week, a five-figure count is unremarkable. To see which names are being tried, group them:
Get-WinEvent -FilterHashtable @{LogName='Security';Id=4625} |
ForEach-Object { $_.Properties[5].Value } |
Group-Object | Sort-Object Count -Descending |
Select-Object -First 10 Count, Name
Thousands of failed logons is normal background noise for an internet-facing Windows machine. It is not evidence that you were breached. It is evidence of how much automated attention the port receives — which is the argument for taking it off the internet.
The count that should worry you is not the failures. It is a successful logon (Event ID 4624, type 10) from an address you do not recognise, or an unexpected account appearing in the local administrators group.
04Close It at the Host, Tunnel In
This is the fix that needs nothing installed anywhere new. You already have SSH access to the Ubuntu host — it is how you installed Windows in the first place — and SSH can carry RDP traffic for you. The port stops being published, and you keep working exactly as before.
Step 1 — Bind the forward to loopback
Edit the start script and change the empty address to 127.0.0.1:
# before
-netdev user,id=net0,hostfwd=tcp::3389-:3389
# after
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:3389-:3389
That single change means the host now accepts RDP traffic only from the machine itself. The Windows guest is untouched. From the internet, the port no longer exists.
Step 2 — Remove the firewall rule
iptables -D INPUT -p tcp --dport 3389 -j ACCEPT
If your host restores firewall rules on boot — some images ship with
iptables-persistent or netfilter-persistent — remove the rule
there as well, or it will come back at the next reboot. You can check with
iptables -S INPUT | grep 3389.
Step 3 — Restart the VM
systemctl restart osip-qemu
Restarting the QEMU service kills the virtual machine the way pulling the plug would, then starts it again. Save anything you have open, and do this at a moment when a hard reboot is acceptable. Windows will go through its normal recovery on the next boot.
Step 4 — Reach it through an SSH tunnel
From the computer you work on, open a tunnel that maps a local port to the server’s loopback:
ssh -L 3389:127.0.0.1:3389 root@YOUR_SERVER_IP
Leave that window open, then point Remote Desktop at localhost:3389 instead of
the public address. The connection travels inside the SSH session, encrypted, to the host,
and from there to the VM. As far as the internet is concerned, the only port on your server
is SSH — which is already the port you were protecting anyway.
The tunnel has to be running before you can connect, so RDP is no longer something you can open from a random computer. For most people that is the point. If you want it to start automatically on Windows, add -N -f to the ssh command and it runs in the background; on Linux or macOS a two-line shell function is enough.
Because the host port binds to loopback, this also means only the host can reach the guest’s RDP. Anything else that needs access — a VPN, a jump host, a monitoring agent — has to run on that host, which is a useful constraint rather than a limitation.
05Tailscale, Briefly
Tailscale builds a private network out of WireGuard and manages the keys for you. You
install it on the Ubuntu host and on the machine you connect from, sign both into the same
account, and each gets a stable private address in the 100.x range. Then you
point Remote Desktop at that address instead of the public IP and remove the firewall rule.
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
That is genuinely the whole setup. The catch is licensing rather than technology, and it is worth reading before you rely on it. From Tailscale’s own pricing page, the free Personal plan
“is for individuals who want to use Tailscale at home. This is a free plan and is only suitable for non-commercial use.”
That covers up to 6 users and unlimited devices, so headcount is not the constraint — purpose is. Signing up with an address on a custom domain is also treated as business use and drops the account into a trial rather than the free plan.
A trading bot, client work or anything commercial is outside the Personal plan. Either pay for the Standard tier, or use the SSH tunnel in section 04 or WireGuard below — both of which have no such condition, because nobody is providing you a service.
06WireGuard and Cloudflare Tunnel
Both are the same idea as Tailscale — a private path instead of a published port — with no third party deciding what your traffic is for.
WireGuard, run by you
WireGuard is the protocol Tailscale is built on. Running it yourself means more setup and fewer strings: no account, no provider, no terms separating personal from commercial, and nobody in the middle who can see which machines you connect to. You already have the hardest thing to get, which is a server with a public IP address.
apt install -y wireguard
cd /etc/wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of server.key>
[Peer]
# your laptop
PublicKey = <your laptop's public key>
AllowedIPs = 10.8.0.2/32
wg-quick up wg0
systemctl enable wg-quick@wg0
Then RDP to 10.8.0.1 once your client is connected, and close port 3389. There
is a proper client for Windows, macOS, Linux, iOS and Android.
WireGuard listens on UDP 51820, so that port does have to be open. The difference is that it stays completely silent to anyone who is not a configured peer: an unauthenticated packet gets no reply at all, so a scanner learns nothing and sees no service. Compare that to RDP, which announces itself by answering.
Cloudflare Tunnel with Zero Trust
This is the opposite approach. Instead of opening a port and authenticating what comes through it, the server makes an outbound connection to Cloudflare and your client connects through that. Nothing on your machine accepts an inbound connection at all, so there is no port to find even if somebody is looking for one.
The useful part is what you can put in front of it. With Zero Trust, access can require an authenticated identity rather than a Windows password — only the addresses you list get through, with a one-time code. That is a second, independent thing to defeat, and it scales better than firewall rules when several people need in: adding a colleague means adding an email address, not teaching them a VPN client. It does mean Cloudflare sits in the path of your remote desktop traffic.
07If You Must Keep It Public
Sometimes there is a reason: you connect from an address that changes, or from a device you cannot install anything on. If the port has to stay reachable, work in this order — host first, guest second, because that is the order in which the layers actually matter.
On the Ubuntu host, over SSH
The installer added a rule that accepts traffic to 3389 from anywhere. Replace it with one that accepts your address and drops the rest. Remove the open rule first — it sits at the top of the chain and would accept everything before your new rules are ever reached.
# remove the rule that accepts 3389 from anywhere
iptables -D INPUT -p tcp --dport 3389 -j ACCEPT
# accept it only from your address
iptables -I INPUT -s 203.0.113.45 -p tcp --dport 3389 -j ACCEPT
# and drop everything else aimed at that port
iptables -A INPUT -p tcp --dport 3389 -j DROP
Order matters: -I inserts at the top, -A appends to the bottom, so
your address is checked before the drop. Confirm with
iptables -S INPUT | grep 3389, and remember the rule is not persistent across
reboots unless your image restores firewall rules — check for
iptables-persistent or netfilter-persistent.
If your provider offers a cloud firewall in its control panel, that is a friendlier place to manage the same thing, and it often survives a reboot on your behalf. Do not do both and forget one of them: a rule you cannot find later is a rule you cannot remove.
This is the closest thing to a real fix while the port stays open, and it is also the fastest way to lose access to your own server. Before you scope the rule, make sure you can reach the host through your provider’s console — not through RDP — so you can undo it. A second administrator account that you never use for daily work is worth having anyway.
Inside Windows, once traffic can reach it
Turn on Network Level Authentication
It requires the user to authenticate before a session is created, which shuts out the bulk of the automated traffic that only gets as far as the login screen. It is the single highest-value setting on this list, and it costs nothing.
FIRSTDeal with the built-in Administrator account
Every password guess in the world is aimed at a handful of usernames, and Administrator is the first one tried. Rename it, disable it, and use a named account instead. This does not make you harder to reach, it makes you harder to guess at.
Set an account lockout policy — and understand the trade
Locking an account after a number of failures stops a slow guessing attack. It also hands an attacker a way to keep you out of your own machine by deliberately failing logins until the account locks. Set the threshold high enough that it is annoying to reach, and keep a second administrator account you never use for daily work.
TRADE-OFFChange the port, knowing what it is
Moving RDP off 3389 removes you from the cheapest scans, and that is genuinely worth something. It is obfuscation, not protection: a full port scan finds the service in seconds, and changing a port is not the same as closing one. Do it last, if at all — and if you do, the port number lives in two places, the guest and the host forward.
LAST RESORTEverything above reduces how attractive your machine is. None of it changes the fact that the port is on the public internet. Treat it as defence in depth while you work out how to stop publishing it.
08Verify It Actually Worked
A change you have not tested is a change you do not have. Three checks, in this order.
nc -vz YOUR_SERVER_IP 3389
Connection refused or a timeout is what you want. If it still succeeds, a firewall
rule is still there. On the host, ss -ltnp | grep 3389 should now show
127.0.0.1:3389 rather than 0.0.0.0:3389 if you made the section 04
change.
Then confirm the thing you were protecting still works: connect through the private path you chose, and check that the remote desktop appears. Finally, leave it a day and re-run the failed-logon count from section 03. If it has stopped climbing, the attention has moved on to somebody else.
Run the port check from a phone on mobile data, or from an online port checker. Testing from the same network as the server — or from the server itself — can succeed for reasons that have nothing to do with whether the internet can reach you.
09Frequently Asked Questions
Is a strong password enough if I leave port 3389 open?
It is necessary but not sufficient. A long unique password defeats guessing, and that is most of what an exposed RDP port faces. What it does not protect you from is a flaw in the RDP service itself, which needs no password at all, or from a password that leaks somewhere else and gets tried here. Taking the port off the internet removes both of those exposures at once.
Can I just change the RDP port instead of all this?
You can, and it does cut the noise, because the cheapest scans only look at 3389. But a scan that checks every port finds the service wherever you moved it, and the change gives you no protection against anyone who is actually looking for you rather than for an opportunity. Treat it as tidying up, not as security. On a VM there is also a second place to change it — the host port forward, not just the Windows setting.
Will closing port 3389 break my OS Installer installation?
No. Windows keeps running and the VM keeps running; you are only changing how you reach the desktop. The one thing to be careful about is order: set up the private path and confirm you can connect through it before you close the public rule, so you are never locked out of your own machine.
Does any of this apply to the Linux SSH port too?
The logic is identical, only the port changes. SSH on 22 gets the same automated attention that RDP on 3389 does. The best thing you can do for SSH is disable password authentication entirely and use keys, which removes brute force as a category rather than making it slower. Since every method in this article depends on SSH, that is worth doing regardless of which one you pick.
How do I know whether someone already got in?
Look for successful logons rather than failed ones: Event ID 4624 with logon type 10 from an address you do not recognise. Check the members of the local Administrators group for accounts you did not create, look at scheduled tasks you did not make, and check for remote desktop sessions that were still open when you were not using it.
My provider has a firewall. Do I still need to do this?
It helps, but check what it is actually doing. Many provider firewalls are a web interface over rules on your own machine, so you are configuring the same thing described here, just from somewhere else. Others are genuinely in front of your server and never pass the traffic. The check in section 03 tells you which situation you are in, and that is the only answer that matters.
Get in first. Then close the door.
OS Installer deploys Windows 10 Pro, Windows 11 Pro, Windows Server 2022 or Server 2025 on your Ubuntu 24.04 server over SSH, and prints your access details when the desktop is ready. How you reach it afterwards is up to you — and section 04 is a good place to start.
