Office VPN + RDP: Secure Remote Desktop Without Exposing RDP to the Internet

Was this helpful?

The business wants “just RDP” because it’s familiar, fast, and works with every legacy Windows thing that refuses to die.
Security wants “no public ports” because they’ve seen what the internet does to an exposed 3389. You’re stuck in the middle, holding a pager.

The good news: you can have remote desktop that feels like RDP, without ever letting RDP touch the public internet.
The bad news: you must be disciplined about network boundaries, identity, and diagnostics—because when it breaks, it breaks at 2 a.m.

The goal: RDP stays private

The core rule is embarrassingly simple: RDP should never be reachable from the public internet.
Not “it’s okay if we rate-limit.” Not “it’s okay if we changed the port.” Not “it’s okay because we’re small.”
It should be unroutable from outside. Full stop.

Instead, users establish a secure tunnel into the office network—typically a VPN—and only then initiate RDP to internal hosts.
That gives you one choke point for authentication, MFA, device controls, and logging. It also gives you one place to break things,
which is… honestly better than breaking them everywhere.

If you’re tempted to publish 3389 with a “temporary” firewall rule, remember: temporary rules have the lifespan of unlabelled leftovers in the office fridge.

Facts and historical context (the stuff that explains today’s mess)

  • RDP has been around since the late 1990s, evolving from Microsoft’s Terminal Services era; it was designed for managed networks, not hostile internet edges.
  • TCP/3389 became a scanning magnet as soon as mass internet scanning became cheap; “security by obscurity” port changes never fixed that.
  • Network Level Authentication (NLA) was introduced to move authentication earlier in the connection flow, reducing resource abuse and some attack surface.
  • BlueKeep (CVE-2019-0708) reminded everyone that pre-auth RDP bugs can be wormable; it wasn’t the first, and it won’t be the last.
  • Ransomware crews operationalized RDP brute forcing because it’s direct and profitable: one credential equals one desktop equals lateral movement.
  • VPNs shifted from “site-to-site plumbing” to “identity front door” as remote work grew; many orgs still treat them like dumb tunnels.
  • Split tunneling became popular to reduce bandwidth costs and avoid hairpinning, and also popular with incident responders who enjoy pain.
  • RD Gateway exists because raw RDP is awkward at scale: it gives central policy and HTTPS transport, but it’s still a published service to protect.
  • Modern remote access is converging on Zero Trust patterns: device posture + per-app access; VPN+RDP can be made to approximate this if you’re strict.

Reference architecture: VPN first, RDP second

Here’s the architecture that tends to survive audits and real attackers:

  1. VPN endpoint on a hardened gateway (or pair, if you like sleep), exposed to the internet on one port (e.g., UDP/51820 for WireGuard).
  2. Strong authentication: certificates/keys plus MFA (or a VPN that integrates with an IdP). If you can’t do MFA, you’re not done.
  3. Internal-only RDP: RDP listens on LAN/VPN interfaces only; firewall rules restrict sources to VPN subnets and only to authorized jump hosts.
  4. Segmentation: users can reach only the hosts they need. “VPN users can access the whole /16” is how you train ransomware to sprint.
  5. Logging: VPN connect/disconnect, RDP logons, and admin actions centralized. If you can’t answer “who accessed host X at time Y,” you’re guessing.
  6. Optional jump box / bastion: one or a small pool of Windows jump hosts with hardened policies. Users RDP to the jump host, then to internal targets.

Why a jump host is often the adult choice

Jump hosts feel old-school, but they force a clean boundary: the VPN gets you to the jump host, and the jump host gets you to everything else.
That means fewer machines need RDP enabled, fewer machines need “Remote Desktop Users” memberships, and fewer machines have to handle random client RDP quirks.

They also centralize logging. Windows can log RDP activity per host, sure, but consolidating access through a small set of hosts makes your incident timeline
less like a scavenger hunt and more like an investigation.

Hard decisions: VPN vs RD Gateway vs both

VPN + internal RDP (the focus here)

Best when you control the client devices, can deploy VPN config cleanly, and want RDP to stay internal.
This is the “don’t expose RDP to the internet” approach in its purest form: the only public-facing component is the VPN endpoint.

RD Gateway (RDP over HTTPS)

RD Gateway wraps RDP in HTTPS (TCP/443), which plays nice with restrictive networks.
It centralizes authorization and can reduce the need for full network access. But it is a public service: you must patch it, monitor it,
and defend it like any other internet-facing system.

Both (yes, sometimes)

Some orgs put RD Gateway behind a VPN anyway. It sounds redundant until you deal with:
(1) contractors who should only access a subset of desktops,
(2) the need for conditional access controls at multiple layers,
(3) compliance regimes that like defense-in-depth when it’s actually configured correctly.

Opinionated take: if you’re a small-to-mid org and you can deploy VPN clients reliably, VPN + jump hosts is the best cost-to-safety ratio.
If you have lots of unmanaged client devices, consider RD Gateway with serious hardening—or better, a per-app access model that avoids broad network access.

Build: WireGuard office VPN (practical, fast, boring)

WireGuard is not magic. It is, however, refreshingly small, fast, and easy to reason about compared to older VPN stacks.
If you run production systems, “easy to reason about” is a feature you can justify on a budget spreadsheet.

Network plan (don’t improvise in prod)

  • VPN subnet: 10.44.0.0/24 (clients live here)
  • Office LAN: 10.10.0.0/16 (servers/desktops live here)
  • WireGuard server: 10.44.0.1
  • Jump hosts: 10.10.20.10–10.10.20.20

Server hardening basics

Put the VPN endpoint on a dedicated VM or appliance, not on the file server, not on the domain controller, and definitely not on “that one box under someone’s desk.”
Keep the OS lean. Patch it. Restrict inbound ports to the VPN port and SSH from trusted admin ranges only.

Lock down RDP on Windows (so it behaves)

Turning on RDP is easy. Turning on RDP safely is where people start bargaining.
You want: NLA enabled, modern TLS, sensible timeouts, limited group membership, and firewall scope limited to the VPN subnet (and maybe the jump host subnet).

Also: disable “just in case” admin accounts. Use separate admin identities. Log. If you can’t log, you can’t defend.

Firewalling and segmentation (where most teams fail)

The VPN is not a magic cloak of safety. It’s a routing event. If VPN clients can route to everything,
then a compromised laptop can route to everything too. Your firewall rules should reflect the job, not the network topology you inherited.

Minimum firewall stance

  • VPN clients can reach only jump hosts on TCP/3389.
  • Admins can reach management ports (WinRM, SSH, etc.) from dedicated admin subnets.
  • Block east-west RDP except where explicitly needed.
  • Log denies from VPN subnet. Not forever, but long enough to investigate incidents.

Split tunneling: choose carefully

Split tunneling means only some traffic goes through the VPN; the rest goes directly to the internet.
It’s good for bandwidth and terrible for assumptions. If your security model relies on “VPN equals trusted network,” split tunneling breaks it.

If you must use split tunneling, treat VPN clients like semi-hostile: restrict what they can reach, require MFA, and log aggressively.

Identity: MFA, device posture, and the myth of “trusted VPN”

VPN access is not identity. It’s connectivity. Identity is: who is this user, from what device, with what assurance, and do they still deserve access right now?

If your VPN supports SSO and conditional access, use it. If it doesn’t, compensate: short-lived credentials, per-user keys, and a separate MFA gate.
For RDP itself, don’t allow local accounts where you can avoid them. Tie it to directory identities and control membership in “Remote Desktop Users.”

A paraphrased idea from Werner Vogels (Amazon CTO): “Everything fails, all the time.” Build remote access as if failure is the default state.
Because it is.

Logging and monitoring: prove who did what

You need two timelines: VPN timeline (who connected, from where, with what device) and Windows timeline (who logged on, where, and what session type).
Put them in the same place. Correlate by username, source IP (VPN IP), and time.

On Windows, you care about logon events (interactive vs remote interactive), failed logons, account lockouts, and group membership changes.
On the VPN gateway, you care about handshake success, config changes, and traffic anomalies.

Practical tasks: 12+ commands, outputs, and decisions

These are the tasks I actually run when I’m building or debugging VPN+RDP in production. Each includes: a command, what output means, and what decision you make next.
Commands are shown from a Linux VPN gateway unless noted. For Windows checks, I’m using PowerShell on a Windows host where needed, but invoked in bash via SSH or a management shell.

Task 1: Confirm WireGuard interface and peer handshakes

cr0x@server:~$ sudo wg show
interface: wg0
  public key: 6xk2n3m...redacted
  listening port: 51820

peer: uRk9hQ...redacted
  endpoint: 198.51.100.24:53312
  allowed ips: 10.44.0.10/32
  latest handshake: 1 minute, 12 seconds ago
  transfer: 148.32 MiB received, 210.77 MiB sent
  persistent keepalive: every 25 seconds

Meaning: “latest handshake” tells you the tunnel is alive; transfer counters tell you traffic is flowing.
Decision: If handshake is “never” or ancient, stop blaming RDP. Fix VPN reachability, keys, NAT, or firewall first.

Task 2: Verify the server is actually listening on the VPN port

cr0x@server:~$ sudo ss -lunpt | grep 51820
udp   UNCONN 0      0            0.0.0.0:51820      0.0.0.0:*    users:(("wg-quick",pid=1123,fd=6))

Meaning: UDP 51820 is bound on all interfaces.
Decision: If nothing is listening, your WireGuard service isn’t up, or it’s bound incorrectly. Fix service and config before anything else.

Task 3: Confirm IP forwarding on the VPN gateway (Linux)

cr0x@server:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

Meaning: The gateway can route packets between interfaces.
Decision: If it’s 0, VPN clients may connect but won’t reach LAN hosts. Enable forwarding and persist it.

Task 4: Check route table for VPN subnet and LAN path

cr0x@server:~$ ip route
default via 203.0.113.1 dev eth0
10.10.0.0/16 via 10.10.0.1 dev eth1
10.44.0.0/24 dev wg0 proto kernel scope link src 10.44.0.1

Meaning: The gateway knows where LAN lives and that wg0 owns the VPN subnet.
Decision: If the LAN route is missing or wrong, your VPN client traffic will blackhole. Fix routing before touching Windows.

Task 5: Verify firewall allows inbound WireGuard and blocks public RDP

cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif "lo" accept
    tcp dport 22 ip saddr 203.0.113.50 accept
    udp dport 51820 accept
    tcp dport 3389 drop
  }
}

Meaning: VPN is allowed; RDP on the gateway itself is explicitly dropped (a nice “belt and suspenders”).
Decision: If you see an accept rule for tcp/3389 on a public interface, remove it. Don’t negotiate with future incidents.

Task 6: Test reachability from gateway to a Windows jump host on 3389

cr0x@server:~$ nc -vz 10.10.20.10 3389
Connection to 10.10.20.10 3389 port [tcp/ms-wbt-server] succeeded!

Meaning: The jump host is reachable and RDP port is open from the gateway network perspective.
Decision: If it fails, you likely have Windows Firewall, network ACLs, or routing issues between gateway and jump subnet.

Task 7: Test reachability from a VPN client IP to jump host (simulate from gateway)

cr0x@server:~$ sudo nft add rule inet filter forward ip saddr 10.44.0.0/24 ip daddr 10.10.20.10 tcp dport 3389 counter accept
cr0x@server:~$ sudo nft list chain inet filter forward
table inet filter {
  chain forward {
    type filter hook forward priority 0; policy drop;
    ip saddr 10.44.0.0/24 ip daddr 10.10.20.10 tcp dport 3389 counter packets 0 bytes 0 accept
  }
}

Meaning: You’ve created an explicit allow rule for VPN clients to the jump host’s RDP, with counters.
Decision: Use counters to confirm traffic hits the rule. If counters stay at 0 during a connection attempt, the traffic isn’t reaching this box (routing/NAT/client config).

Task 8: Confirm Windows has RDP enabled and NLA required (PowerShell on jump host)

cr0x@server:~$ ssh admin@10.10.20.10 'powershell -NoProfile -Command "Get-ItemProperty -Path \"HKLM:\System\CurrentControlSet\Control\Terminal Server\" -Name fDenyTSConnections; Get-ItemProperty -Path \"HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\" -Name UserAuthentication"'
fDenyTSConnections : 0
PSPath              : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server
PSParentPath        : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control
PSChildName         : Terminal Server
PSDrive             : HKLM
PSProvider          : Microsoft.PowerShell.Core\Registry

UserAuthentication : 1
PSPath             : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp
PSParentPath       : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server\WinStations
PSChildName        : RDP-Tcp
PSDrive            : HKLM
PSProvider         : Microsoft.PowerShell.Core\Registry

Meaning: fDenyTSConnections=0 means RDP enabled; UserAuthentication=1 means NLA required.
Decision: If NLA is off, enable it unless you have a documented reason (and a plan) to run without it.

Task 9: Check Windows Firewall rule scope for RDP (avoid “Any”)

cr0x@server:~$ ssh admin@10.10.20.10 'powershell -NoProfile -Command "Get-NetFirewallRule -DisplayGroup \"Remote Desktop\" | Get-NetFirewallAddressFilter | Select-Object Name,RemoteAddress | Format-Table -AutoSize"'
Name                                   RemoteAddress
----                                   -------------
RemoteDesktop-UserMode-In-TCP           Any
RemoteDesktop-UserMode-In-UDP           Any
RemoteDesktop-Shadow-In-TCP             Any

Meaning: “Any” remote address means anyone who can route to this host can attempt RDP.
Decision: Change scope to VPN subnet (10.44.0.0/24) and/or jump subnet only. “Any” is fine in a lab; it’s lazy in prod.

Task 10: Validate that RDP is not exposed publicly (external scan from the gateway’s public side)

cr0x@server:~$ nmap -Pn -p 3389 203.0.113.10
Starting Nmap 7.94 ( https://nmap.org ) at 2025-12-28 15:42 UTC
Nmap scan report for 203.0.113.10
Host is up (0.012s latency).

PORT     STATE  SERVICE
3389/tcp closed ms-wbt-server

Nmap done: 1 IP address (1 host up) scanned in 0.21 seconds

Meaning: Closed is good here. “Filtered” is also acceptable (often better), depending on policy.
Decision: If it’s open, stop. Fix perimeter firewall/NAT immediately. Do not proceed until 3389 is not reachable from the internet.

Task 11: Check DNS behavior for VPN clients (the sneaky cause of “RDP is down”)

cr0x@server:~$ dig +short jump01.corp.example A @10.10.0.53
10.10.20.10

Meaning: Internal DNS resolves the jump host to an internal IP.
Decision: If VPN clients use public DNS, they may resolve to a public record or nothing at all. Fix VPN DNS push or client resolver policy.

Task 12: Confirm client can route to LAN via VPN (from VPN gateway, inspect conntrack)

cr0x@server:~$ sudo conntrack -L | grep 10.44.0.10 | head
tcp      6 431999 ESTABLISHED src=10.44.0.10 dst=10.10.20.10 sport=53422 dport=3389 src=10.10.20.10 dst=10.44.0.10 sport=3389 dport=53422 [ASSURED] mark=0 use=1

Meaning: The RDP TCP session exists through the gateway; packets are flowing and tracked.
Decision: If you don’t see an established flow when the user tries to connect, the problem is earlier (client config, routing, firewall).

Task 13: Check for MTU/fragmentation issues (classic “VPN works but RDP is awful”)

cr0x@server:~$ ping -c 3 -M do -s 1372 10.44.0.10
PING 10.44.0.10 (10.44.0.10) 1372(1400) bytes of data.
1380 bytes from 10.44.0.10: icmp_seq=1 ttl=64 time=34.2 ms
1380 bytes from 10.44.0.10: icmp_seq=2 ttl=64 time=33.5 ms
1380 bytes from 10.44.0.10: icmp_seq=3 ttl=64 time=33.9 ms

--- 10.44.0.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 33.5/33.8/34.2/0.3 ms

Meaning: A 1400-byte packet with DF set passes, suggesting MTU is not obviously broken for that path.
Decision: If it fails with “Frag needed,” tune WireGuard MTU (often 1280–1420 range depending on uplinks) and retest.

Task 14: Verify Windows event logs show RDP logons (prove it’s auth vs network)

cr0x@server:~$ ssh admin@10.10.20.10 'powershell -NoProfile -Command "Get-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4624} -MaxEvents 5 | Select-Object TimeCreated,Id,Message | Format-List"'
TimeCreated : 12/28/2025 3:41:02 PM
Id          : 4624
Message     : An account was successfully logged on.
...
Logon Type: 10
...

Meaning: Logon Type 10 indicates RemoteInteractive (RDP). You have proof that the user actually reached and authenticated to the host.
Decision: If users complain but you see successful logons, the issue is session performance, profile load, GPO, or application slowness—not “VPN down.”

Task 15: Check failed logons and lockouts (brute force or fat-finger)

cr0x@server:~$ ssh admin@10.10.20.10 'powershell -NoProfile -Command "Get-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4625} -MaxEvents 5 | Select-Object TimeCreated,Id,Message | Format-List"'
TimeCreated : 12/28/2025 3:39:11 PM
Id          : 4625
Message     : An account failed to log on.
...
Status: 0xC000006D
Sub Status: 0xC000006A
...

Meaning: Substatus 0xC000006A typically indicates bad password. Repeated events may indicate brute force or a user with sticky keys (the human kind).
Decision: If it’s a real user, reset credentials or fix saved passwords. If it’s unknown, investigate source IP (VPN IP), disable account, and look for lateral movement.

Task 16: Confirm RDP service is healthy and listening on Windows

cr0x@server:~$ ssh admin@10.10.20.10 'powershell -NoProfile -Command "Get-Service TermService | Format-Table Status,Name,DisplayName -AutoSize; netstat -ano | findstr \":3389\""'
Status Name        DisplayName
------ ----        -----------
Running TermService Remote Desktop Services
  TCP    0.0.0.0:3389           0.0.0.0:0              LISTENING       1204

Meaning: RDP service is running and port is listening.
Decision: If service is stopped or port not listening, you’re not chasing VPN ghosts—you’re fixing the host.

Fast diagnosis playbook (find the bottleneck quickly)

When remote desktop is “down,” the fastest path is to stop treating it as one system. It’s four systems wearing a trench coat:
client device, VPN, network path, and Windows session.

First: Is the VPN tunnel actually up?

  • On the gateway: wg show and check “latest handshake.”
  • On the client: confirm it has a VPN IP (e.g., 10.44.0.x) and routes for internal subnets.
  • Decision: If handshake is stale, fix VPN reachability (NAT, keys, UDP blocked). Don’t touch Windows yet.

Second: Can you reach the jump host by IP on 3389?

  • From gateway or a test client on VPN: nc -vz 10.10.20.10 3389.
  • Decision: If port is closed, it’s firewalling or service state. If open, keep going.

Third: Is it DNS, authentication, or session setup?

  • DNS: dig jump01... and compare to expected internal IP.
  • Auth: check Windows Security logs for 4624/4625 around the time of the attempt.
  • Session performance: if logon succeeded but UI is unusable, suspect MTU, packet loss, profile load, roaming profiles, or GPO scripts.

Fourth: If it’s slow, measure the network—don’t guess

  • Ping with DF and sized packets to catch MTU.
  • Look at WireGuard transfer counters and interface errors.
  • Decision: If latency is fine but UI is laggy, suspect server CPU/RAM/disk contention (yes, storage can ruin RDP).

Common mistakes: symptoms → root cause → fix

1) Symptom: “RDP works on office Wi‑Fi, fails remotely”

Root cause: RDP firewall rules allow LAN subnets but not VPN subnets; or VPN routes aren’t pushed.
Fix: Scope Windows Firewall “Remote Desktop” rules to include 10.44.0.0/24, and ensure VPN server routes LAN networks to clients.

2) Symptom: “VPN connects, but hostname RDP fails; IP works”

Root cause: DNS not configured for VPN clients, or split tunneling sends DNS queries to public resolvers.
Fix: Push internal DNS servers in VPN config; verify with dig against internal DNS and client resolver settings.

3) Symptom: “RDP prompts for credentials repeatedly”

Root cause: NLA mismatch, time skew affecting Kerberos, or using local accounts with policies that block remote logon.
Fix: Require NLA, ensure clients support it, fix time sync (NTP), and use domain accounts with proper group membership.

4) Symptom: “RDP connects but is painfully laggy, especially typing”

Root cause: MTU/fragmentation issues over VPN, packet loss, or bufferbloat on uplinks.
Fix: Test with DF pings; adjust WireGuard MTU; add sensible QoS/traffic shaping; avoid VPN hairpinning when possible.

5) Symptom: “Random disconnects every few minutes”

Root cause: NAT timeouts and missing keepalive on roaming clients; unstable Wi‑Fi; aggressive firewall state timeouts.
Fix: Enable WireGuard persistent keepalive for clients behind NAT; investigate firewall/ISP behavior; prefer wired where practical.

6) Symptom: “Everyone can RDP everywhere once on VPN”

Root cause: Flat network + permissive firewalling + broad AD group membership.
Fix: Implement segmentation: VPN clients to jump hosts only; least privilege RDP groups; deny-by-default forward policies.

7) Symptom: “Security says we must rotate keys weekly, now nothing works”

Root cause: Rotation without automation; clients out of sync; stale peer configs.
Fix: Automate provisioning/rotation; use short-lived auth where possible; version configs and roll out with device management.

Three corporate mini-stories (because scars teach)

Mini-story 1: The incident caused by a wrong assumption

A mid-sized company rolled out a VPN to “fix” remote access. They did the basics: a WireGuard server, user keys, a clean subnet.
The team assumed that because users had to authenticate to the VPN, everything behind it was “internal” and therefore safe.

They left Windows Firewall RDP rules at their default scope (“Any”), and their internal network was mostly flat.
The helpdesk liked it: fewer tickets. The security team didn’t notice because nothing was technically “internet-facing.”

Then a contractor laptop got compromised via a browser exploit. The attacker didn’t need to scan the public internet.
They connected to the VPN using the contractor’s already-configured client, landed on the VPN subnet, and immediately started probing internal RDP.
A few weak passwords later, they had a desktop with access to file shares and some admin tools.

The post-incident meeting was quietly brutal. The problem wasn’t WireGuard. The problem was the assumption that “VPN equals trusted.”
The fix was equally unglamorous: restrict VPN-to-LAN traffic to jump hosts, enforce MFA, and scope RDP firewall rules to specific sources.
Security got their containment. Helpdesk got a new script. Everyone got a longer on-call runbook.

Mini-story 2: The optimization that backfired

Another org had complaints: “RDP is slow.” They looked at VPN bandwidth graphs, saw spikes, and decided split tunneling would “reduce load.”
They pushed a config change Friday afternoon (of course) so only 10.10.0.0/16 went through the VPN; everything else went out locally.

Monday morning: half the users couldn’t RDP by hostname. The other half could connect but got weird authentication prompts.
It wasn’t random. Some home networks hijacked DNS. Some users had “helpful” ISP resolvers. A few had old corporate DNS entries cached.

Worse: a subset of users now had RDP sessions while their general internet traffic bypassed corporate controls.
A malware infection on a home network had a cleaner path to command-and-control while the infected device still had a route into the corporate LAN via VPN.
That’s the kind of sentence you don’t want to write in an incident report.

They rolled back split tunneling for most users and reintroduced it only for a tightly controlled group with managed endpoints and strict ACLs.
Performance improved later, but not from split tunneling. It improved because they fixed MTU issues and implemented QoS on the office uplink.
The “optimization” wasn’t wrong in theory. It was wrong in context.

Mini-story 3: The boring but correct practice that saved the day

A regulated enterprise ran remote access through VPN + a small pool of Windows jump hosts.
Every quarter, they did something that looked like paperwork: they reviewed group memberships for remote access,
validated firewall scopes, and tested that no RDP ports were reachable externally.

During one review, an engineer noticed that a new subnet had been added for a lab environment.
The routing team had accidentally advertised it broadly, and a permissive firewall rule meant VPN clients could now reach lab servers directly.
Lab servers, naturally, were “temporary,” “experimental,” and patched on a schedule best described as “aspirational.”

They fixed it before it became exciting: tightened the forward rules, updated address groups, and required lab access to go through a separate jump host with extra controls.
Two weeks later, a vulnerability dropped for the lab’s hypervisor management UI. The lab got scanned. Nothing happened.
The best incident is the one that never becomes an incident, and it’s almost always prevented by someone doing something boring on purpose.

Checklists / step-by-step plan

Step-by-step plan to deploy VPN + RDP safely

  1. Pick a remote access model: VPN + jump host for most orgs; RD Gateway if you need per-app access without full network reach.
  2. Allocate subnets: a dedicated VPN client subnet; don’t reuse LAN ranges or you’ll regret it in coffee shops and hotels.
  3. Build the VPN gateway: hardened OS, minimal services, patching pipeline, configuration management.
  4. Implement deny-by-default forwarding: only allow VPN clients to reach jump hosts on 3389 (and DNS if needed).
  5. Deploy jump hosts: small pool, hardened baseline, restricted admin access, logging enabled.
  6. Lock down RDP on targets: enable NLA, restrict firewall scope, disable legacy ciphers where possible, keep OS patched.
  7. Add MFA: at VPN entry, and ideally also at Windows sign-in with conditional access or equivalent controls.
  8. Centralize logs: VPN logs + Windows security logs + firewall logs in one place with retention that matches your risk.
  9. Test “no public RDP” continuously: scheduled external scans or monitoring from a controlled vantage point.
  10. Document fast diagnosis: the on-call path should take minutes, not a Slack archaeology expedition.

Operational checklist (weekly/monthly)

  • Confirm 3389 is not open on public IPs (scan from outside).
  • Review VPN peer list and remove stale users/devices.
  • Review “Remote Desktop Users” and local Administrators memberships on jump hosts.
  • Patch jump hosts and VPN gateways on a schedule that exists in reality.
  • Spot-check logs: failed VPN auth, repeated RDP failures, unusual source geos (if applicable).
  • Test a real remote connection from a non-office network.

FAQ

1) Why is exposing RDP to the internet such a big deal if I use strong passwords?

Because RDP is a high-value target and gets hammered by scanners and credential stuffing constantly.
Strong passwords help, but they don’t eliminate protocol vulnerabilities, misconfigurations, or stolen credentials.
The safest RDP is the RDP the internet can’t reach.

2) Isn’t changing the RDP port enough?

No. It reduces noise from the laziest scanners, not the risk from anyone competent.
Attackers scan the whole port range or fingerprint services. Don’t build security on the hope they’ll get bored.

3) VPN feels like giving users full network access. Can I limit it?

Yes, and you should. Deny-by-default forwarding on the VPN gateway plus explicit allow rules to jump hosts is the standard move.
If your VPN product supports per-user ACLs, use them. If not, segment by groups and subnets.

4) Should I use UDP or TCP VPN transport?

Prefer UDP where possible (WireGuard is UDP). TCP-over-TCP can amplify latency and retransmission weirdness.
If you’re forced into TCP by captive networks, consider an alternative access path, but don’t redesign everything around hotel Wi‑Fi.

5) Do I need an RD Gateway if I already have a VPN?

Not necessarily. RD Gateway is useful when you need RDP access without full network routing, or when client environments block VPNs.
But it’s another public service to operate. If VPN deployment is feasible, VPN + jump hosts is simpler and usually safer.

6) How do I stop a compromised VPN client from scanning my LAN?

Don’t let it. Implement firewall rules so VPN clients can only reach specific destinations and ports (usually jump hosts on 3389).
Log denies. Consider separate admin VPN pools. Treat clients as untrusted until proven otherwise.

7) RDP is slow only over VPN. What’s the most common cause?

MTU issues and packet loss are top contenders. RDP is interactive; small delays feel huge.
Test DF pings, tune VPN MTU, and check uplink congestion. Also check the server: disk contention and profile loading can mimic “network slowness.”

8) Can I rely on Windows Firewall alone?

You can, but you’ll be happier if you don’t. Defense-in-depth matters: perimeter firewall/NAT, VPN gateway ACLs, and Windows Firewall scopes.
Windows Firewall is good, but it’s also one policy mistake away from “Any.”

9) What do I do about vendors/contractors who need temporary access?

Give them access to a dedicated jump host with restricted tooling and time-bound credentials.
Avoid giving them broad VPN routes. Log their sessions. Remove access automatically when the contract ends (calendar reminders are not automation).

10) Is MFA on the VPN enough, or do I need MFA on Windows too?

MFA at the VPN is the minimum. MFA on Windows (or conditional access at the session boundary) is better, especially for privileged users.
The goal is to make stolen VPN credentials insufficient to reach sensitive desktops or admin sessions.

Conclusion: next steps you can actually do

If you want secure remote desktop, stop thinking about RDP as the product. The product is controlled access.
VPN is the front door. Firewalling is the hallway. Jump hosts are the reception desk. Logging is the security camera footage you’ll need later.

Next steps that move the needle this week:

  1. Scan your public IPs and verify TCP/3389 is closed/filtered everywhere.
  2. Restrict RDP reachability to VPN subnets and ideally only to jump hosts.
  3. Turn on NLA and confirm it in registry/policy.
  4. Add MFA to VPN access, and limit VPN routes to what users actually need.
  5. Centralize logs for VPN connections and Windows RDP logons so you can answer questions with evidence, not vibes.

Second joke, because you earned it: The internet doesn’t “discover” an exposed RDP port; it just remembers where you live.

← Previous
Docker Port Published but Unreachable: The Real Checklist (Not Guesswork)
Next →
Ubuntu 24.04 “Temporary failure in name resolution”: stop guessing and fix DNS the right way

Leave a comment