VPN + RDP/SSH: Remote Access Without Opening Ports to the Internet

Was this helpful?

Somewhere right now, an admin is staring at a firewall rule that says “TEMP: 3389 ANY-ANY,” trying to remember what panic felt like before they got into IT.

If you need remote access to servers and desktops, you do not need to spray RDP or SSH across the public internet. You need a private path, predictable identity, and a way to debug it at 2 a.m. without guessing.

Why you shouldn’t open RDP/SSH to the internet

Opening RDP (3389) or SSH (22) to the internet is like leaving your office badge reader outside the building, taped to a lamppost, with a sticky note that says “please be normal.” People will not be normal.

Yes, you can harden. Yes, you can geo-block. Yes, you can add MFA. But you’re still advertising an attack surface that’s always-on, globally reachable, and constantly scanned. Even if the authentication is strong, the service itself becomes a target: protocol bugs, pre-auth issues, downgrade weirdness, credential stuffing, password spraying, and all the “it’s just a PoC” chaos that becomes production incident fuel.

Putting RDP/SSH behind a VPN changes the game in three ways:

  • Reachability: services are no longer publicly reachable, so most opportunistic attacks never start.
  • Identity boundary: the first gate becomes the VPN authentication and device posture, not the target service.
  • Observability: you can log and rate-limit at one choke point. One place to look, one place to lock.

That doesn’t mean “VPN equals secure.” It means you can build something sane: private routing, least privilege, and audit trails that don’t read like a crime novel written by a load balancer.

Interesting facts and quick history

Context matters. A lot of today’s remote access pain is yesterday’s “ship it” decision.

  1. RDP wasn’t designed for hostile networks. It grew up in enterprise LANs and WANs, not in the modern internet threat model.
  2. SSH replaced telnet/rsh because plaintext died. SSH’s early adoption in the late 1990s was a practical response to “people can sniff your passwords.”
  3. IPsec predates most cloud habits. IPsec has been around since the 1990s; many “new” VPN debates are old arguments in new clothes.
  4. SSL VPNs took off because NAT exists. When everything sits behind NAT and firewalls, tunneling over TCP/443 became a survival tactic.
  5. RDP brute-force became an economy. Botnets love scanning 3389 because it’s reliably profitable: credentials, ransomware staging, lateral movement.
  6. WireGuard is young but opinionated. It’s deliberately small, modern cryptography, and fewer knobs. That’s a feature for ops—less to misconfigure.
  7. Split tunneling is an old fight. Security wants it off. Networking wants it on. The answer is usually “it depends,” but not as often as people think.
  8. Bastion hosts are basically a modern dial-up jump box. The idea is ancient: one controlled door into a private network.
  9. NLA (Network Level Authentication) saved RDP’s reputation. It moved authentication earlier in the connection, reducing exposure to some classes of attacks.

A reference architecture that works in the real world

Here’s the architecture I recommend when you want remote administration (RDP/SSH) without opening those ports to the world. It scales down to a 10-person company and up to the “we have three VPN products because of mergers” corporation.

Goal: make the private network the only network that matters

The core idea is simple: your admin endpoints (laptops) join a VPN. Your managed systems (servers/desktops) are reachable only from VPN address space or internal subnets. RDP/SSH are bound to internal interfaces. Firewalls enforce it.

Minimal moving parts (but no heroics)

  • VPN gateway: WireGuard or OpenVPN (or IPsec), ideally redundant.
  • Identity provider: at minimum local accounts + MFA; better is SSO integration on the VPN layer.
  • Routing: either routed VPN (L3) or a combination of policy routes and NAT. Prefer routed.
  • Access control: per-user or per-group allowed IP ranges; ideally enforced in firewall and/or VPN config.
  • Logging: VPN connect/disconnect, authentication events, and session metadata. For SSH, record commands where feasible.

Where the jump host fits (and where it doesn’t)

Some teams treat “VPN” and “bastion host” as mutually exclusive. They aren’t. A clean pattern is:

  • Users VPN in.
  • Users can only reach a hardened jump host (SSH) and maybe a remote desktop gateway.
  • From the jump host, they reach deeper networks based on least privilege.

This reduces blast radius if a laptop is compromised and gives you a place to enforce session recording. But don’t put a jump host in just to avoid fixing routing. That’s not architecture; that’s penance.

One quote that should live in your head: “Hope is not a strategy.” (paraphrased idea often attributed to operational leaders)

VPN options that don’t hate you back

WireGuard: my default for modern admin access

WireGuard is small, fast, and aggressively minimalist. For operations, that means fewer configuration states that work “sometimes.” Keys are static; identity is more like “this device key is allowed.” You can layer SSO and device posture outside of WireGuard if you use a controller, but even plain WireGuard is a solid base.

Trade-offs:

  • Pros: performance, stability, simple configs, clean cryptography, easy routing.
  • Cons: key management is on you unless you use a management layer; revocation is “remove peer” (fine, but you need process).

OpenVPN: battle-tested, flexible, sometimes chatty

OpenVPN remains common because it works almost everywhere and supports lots of auth mechanisms. It’s heavier than WireGuard and can be trickier to tune, but it integrates well in traditional enterprise setups.

Trade-offs:

  • Pros: mature ecosystem, many auth integrations, TCP/443 option for hostile networks.
  • Cons: more knobs, more complexity, performance overhead.

IPsec (IKEv2): excellent when you have good network hygiene

IPsec is great when you can standardize and you care about interoperability. It can also be great when you need site-to-site tunnels between networks rather than individual admins.

The problem is less the protocol and more the reality: vendors interpret things differently, NAT traversal sometimes feels like folklore, and debugging can turn into a staring contest with packet captures.

Don’t confuse “remote desktop gateway” with “VPN replacement”

RDP gateways can be useful. They can also become a single shiny target if you publish them to the internet. If you do expose a gateway publicly, treat it like a production API: hardened OS, MFA, rate limiting, strict TLS, vulnerability patching, and constant logging. Otherwise: keep the gateway behind the VPN too.

Joke #1: A firewall rule that says “temporary” is the only thing in IT that outlives the hardware.

Identity, access control, and “who did what”

Authentication: make the VPN the hard part

If you put RDP/SSH behind VPN, the VPN login becomes your front door. Make it strong:

  • MFA for VPN users. Not optional. If you can’t do MFA, you’re not doing remote access; you’re doing remote gambling.
  • Device trust if you can: managed devices only, certificates, or a posture check.
  • Short-lived access for vendors and emergencies: time-boxed accounts or temporary group membership.

Authorization: routing is a permission system

The easiest way to leak access is to hand out a VPN profile that routes everything. Congrats, you built a flat network with nicer encryption.

Instead:

  • Define VPN address pools by role (admins, support, vendors).
  • Restrict which internal subnets each pool can reach using firewall rules on the VPN gateway and/or internal firewalls.
  • For very sensitive environments, add a second gate: only the jump host is reachable, and that host enforces per-target access.

Auditing: you’ll want it later, so build it now

“Who connected, from where, and what did they touch?” is never asked during calm weeks. You’ll hear it when something breaks, or when Legal starts being politely intense.

At minimum, collect:

  • VPN connection logs (user/device identity, source IP, assigned VPN IP, start/stop time).
  • RDP logs on Windows (logon events, session start/stop).
  • SSH auth logs and ideally command auditing for privileged access.

Hardening RDP and SSH once they’re behind a VPN

Bind services to internal interfaces

Your ideal state is: even if someone accidentally opens a firewall, the service still doesn’t listen on the public interface. Defense in depth, not defense in wishful thinking.

SSH hardening that actually matters

  • Keys over passwords for admin accounts.
  • No root login over SSH. Use sudo with logging.
  • Limit users with AllowUsers or AllowGroups.
  • Use modern crypto (most defaults are OK on current distros; don’t get creative).

RDP hardening that actually matters

  • Enable NLA and require strong authentication.
  • Use Remote Desktop Users group intentionally; don’t throw “Domain Users” in there because you’re tired.
  • Limit clipboard/drive redirection if you handle sensitive data. Convenience is how data escapes.
  • Patch aggressively. RDP-related bugs are not theoretical collectibles.

Segment management networks

If you can, keep a dedicated management subnet. Your production app subnets should not be the place where admins casually browse servers via RDP. That’s how malware learns your internal map.

Practical tasks with commands: verify, diagnose, decide

You don’t troubleshoot remote access with feelings. You troubleshoot it with packet paths, routing tables, and logs. Below are real tasks you can run today. Each includes what the output means and the decision you make next.

Task 1: Confirm your VPN interface and assigned address (Linux)

cr0x@server:~$ ip -brief addr show
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             203.0.113.10/24 fe80::1c2:abff:fe3d:111/64
wg0              UP             10.42.0.2/32

What it means: wg0 exists and has 10.42.0.2. If it’s missing or DOWN, you’re not “on VPN.”

Decision: If wg0 is missing, start the service and check config; if it’s UP but no traffic flows, move to routing/firewall tasks.

Task 2: Check WireGuard handshake and last activity

cr0x@server:~$ sudo wg show
interface: wg0
  public key: gD2RkqvB8sH9m3Q0uP5xPpZQmYQbqgG2vNQhJwRzG2A=
  listening port: 51820

peer: N7u2dX3XyqS3pWm0pYbV5z6mXh3q3wVxQe7zq7LwG3M=
  endpoint: 198.51.100.24:53321
  allowed ips: 10.42.0.1/32
  latest handshake: 34 seconds ago
  transfer: 42.11 MiB received, 18.07 MiB sent

What it means: A recent handshake proves crypto and basic connectivity. Transfer counters changing means actual traffic passes.

Decision: If handshakes are stale, check NAT/firewall/port reachability. If handshakes are fresh but RDP/SSH fails, it’s routing or service binding.

Task 3: Validate route to an internal subnet via VPN

cr0x@server:~$ ip route get 10.10.20.15
10.10.20.15 dev wg0 src 10.42.0.2 uid 1000
    cache

What it means: The kernel plans to send to 10.10.20.15 via wg0.

Decision: If it says dev eth0 or “unreachable,” fix VPN AllowedIPs/routes before touching RDP/SSH configs.

Task 4: Test basic reachability (ICMP) and interpret carefully

cr0x@server:~$ ping -c 3 10.10.20.15
PING 10.10.20.15 (10.10.20.15) 56(84) bytes of data.
64 bytes from 10.10.20.15: icmp_seq=1 ttl=63 time=23.4 ms
64 bytes from 10.10.20.15: icmp_seq=2 ttl=63 time=22.9 ms
64 bytes from 10.10.20.15: icmp_seq=3 ttl=63 time=23.1 ms

--- 10.10.20.15 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 22.9/23.1/23.4/0.2 ms

What it means: The path works and latency is reasonable. If ping fails, it might still be fine: ICMP is often blocked.

Decision: If ping succeeds but TCP doesn’t, focus on service ports and firewalls. If ping fails, move to traceroute and firewall rules.

Task 5: Test TCP port reachability for SSH

cr0x@server:~$ nc -vz 10.10.20.15 22
Connection to 10.10.20.15 22 port [tcp/ssh] succeeded!

What it means: Something is listening and reachable on port 22.

Decision: If it fails, check local service listening state and firewall policies. Don’t “just open it” globally; open it only from VPN CIDRs.

Task 6: Test TCP port reachability for RDP

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

What it means: RDP is reachable on the internal address. If RDP client still fails, it’s auth/NLA/cert policy, not network reachability.

Decision: If this fails but ping works, you’re looking at a firewall on host, subnet ACL, or service not running.

Task 7: Confirm SSH daemon listens only on internal/VPN interfaces (Linux)

cr0x@server:~$ sudo ss -lntp | grep ':22'
LISTEN 0      128        10.10.20.15:22        0.0.0.0:*    users:(("sshd",pid=923,fd=3))
LISTEN 0      128           10.42.0.10:22        0.0.0.0:*    users:(("sshd",pid=923,fd=4))

What it means: SSH listens on internal and VPN addresses, not on 0.0.0.0:22 (all interfaces).

Decision: If you see 0.0.0.0:22 on a host with a public interface, bind to specific addresses or use firewall rules to strictly limit sources.

Task 8: Validate firewall rules on the VPN gateway (nftables)

cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    iif "lo" accept
    ct state established,related accept
    iif "wg0" tcp dport { 22, 3389 } accept
    iif "wg0" icmp type echo-request accept
    tcp dport 51820 accept
    reject with icmpx type port-unreachable
  }
}

What it means: Default drop. Only VPN interface wg0 can reach SSH/RDP on this gateway. WireGuard port is allowed.

Decision: If you see broad accepts on iif "eth0" for 22/3389, fix it. If policy is drop and you forgot ct state established, you’ll create “works for 2 seconds” nonsense.

Task 9: Check IP forwarding and rp_filter (classic routing footgun)

cr0x@server:~$ sysctl net.ipv4.ip_forward net.ipv4.conf.all.rp_filter
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 2

What it means: Forwarding is enabled. Reverse path filtering is “loose” (2), usually correct for VPN routing with asymmetric paths.

Decision: If forwarding is 0 on a routing gateway, enable it. If rp_filter is strict (1) and you see weird one-way traffic, set it to 2 for relevant interfaces.

Task 10: Verify NAT rules if you must NAT (iptables)

cr0x@server:~$ sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-A POSTROUTING -s 10.42.0.0/24 -o eth1 -j MASQUERADE

What it means: VPN clients in 10.42.0.0/24 are masqueraded when going out eth1 toward internal networks that don’t know the VPN route.

Decision: Prefer adding proper routes instead of NAT. But if you can’t change internal routing quickly, NAT can be a stopgap. Document it like it’s a loaded firearm.

Task 11: Confirm the target Windows host is actually listening on RDP

cr0x@server:~$ sudo nmap -Pn -p 3389 10.10.30.25
Starting Nmap 7.94 ( https://nmap.org ) at 2025-12-27 10:20 UTC
Nmap scan report for 10.10.30.25
Host is up (0.024s latency).

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

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

What it means: Port is open. If your RDP client errors out, it’s negotiation/auth/cert/NLA, not a closed port.

Decision: If it’s closed, check Windows firewall rules, group policy, and whether Remote Desktop is enabled.

Task 12: Confirm SSH authentication failures vs network failures

cr0x@server:~$ ssh -vvv admin@10.10.20.15
OpenSSH_9.6p1 Ubuntu-3ubuntu13, OpenSSL 3.0.13 30 Jan 2024
debug1: Connecting to 10.10.20.15 [10.10.20.15] port 22.
debug1: Connection established.
debug1: Authenticating to 10.10.20.15:22 as 'admin'
debug1: Offering public key: /home/cr0x/.ssh/id_ed25519 ED25519 SHA256:4tS...
debug1: Authentications that can continue: publickey
debug1: Authentication succeeded (publickey).
debug1: Entering interactive session.

What it means: Network is fine and auth succeeded. If you instead see timeouts, that’s network/routing/firewall. If you see “Permission denied,” that’s keys/users/policies.

Decision: Route auth issues to IAM/key distribution. Route timeout issues to network/SRE.

Task 13: Find where packets die (traceroute over VPN)

cr0x@server:~$ traceroute -n 10.10.20.15
traceroute to 10.10.20.15 (10.10.20.15), 30 hops max, 60 byte packets
 1  10.42.0.1  24.102 ms  23.881 ms  23.770 ms
 2  10.10.0.1  24.231 ms  24.021 ms  23.996 ms
 3  10.10.20.15  23.902 ms  23.912 ms  23.901 ms

What it means: You can see the VPN gateway and internal router hops. If it stops at hop 1, the gateway can’t route onward.

Decision: Where it stops is where you look: routing table, ACL, or missing return route.

Task 14: Watch live traffic on the VPN gateway (tcpdump)

cr0x@server:~$ sudo tcpdump -ni wg0 host 10.42.0.2 and host 10.10.20.15 and tcp port 22
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
10:23:11.102334 IP 10.42.0.2.52144 > 10.10.20.15.22: Flags [S], seq 341234112, win 64240, options [mss 1420,sackOK,TS val 123 ecr 0], length 0
10:23:11.125883 IP 10.10.20.15.22 > 10.42.0.2.52144: Flags [S.], seq 91234122, ack 341234113, win 65160, options [mss 1360,sackOK,TS val 456 ecr 123], length 0

What it means: You see SYN and SYN-ACK. That proves return traffic exists at least on wg0. If you only see SYN, return path is broken.

Decision: If return path is missing, check internal routes back to VPN subnet or NAT behavior.

Task 15: Confirm that internal networks have a route back to the VPN subnet

cr0x@server:~$ ip route | grep 10.42.0.0
10.42.0.0/24 dev wg0 proto kernel scope link src 10.42.0.1

What it means: The VPN gateway knows the VPN subnet is on wg0. This is necessary but not sufficient: internal routers must also know where 10.42.0.0/24 lives.

Decision: If internal routers don’t have a return route, add it (best) or NAT (temporary). One-way VPN is a classic time-waster.

Task 16: Check OpenVPN server status if that’s your stack

cr0x@server:~$ sudo systemctl status openvpn-server@server
● openvpn-server@server.service - OpenVPN service for server
     Loaded: loaded (/lib/systemd/system/openvpn-server@.service; enabled; preset: enabled)
     Active: active (running) since Sat 2025-12-27 09:41:02 UTC; 42min ago
       Docs: man:openvpn(8)
   Main PID: 1234 (openvpn)
      Tasks: 1 (limit: 18954)
     Memory: 9.6M
        CPU: 1.214s

What it means: It’s running. If users can’t connect, check logs and client config, not the service state.

Decision: If not active, treat it like an outage: restore config, validate certs/keys, and confirm firewall rules.

Fast diagnosis playbook

This is the playbook for “VPN is up but I can’t RDP/SSH” and you need answers before your coffee cools.

First: prove the VPN is actually connected and exchanging traffic

  • Check interface exists and has an IP (ip -brief addr).
  • Check handshake (WireGuard: wg show; OpenVPN: service status + logs).
  • Check transfer counters increase when you attempt a connection.

If handshake is dead: look at reachability to the VPN endpoint (firewall, NAT, UDP blocked, wrong port).

Second: verify routing in both directions

  • From client: ip route get <target> must say it uses the VPN interface.
  • From VPN gateway: confirm it can route to target subnet.
  • From internal router: confirm it has a return route to VPN subnet (or you’re doing NAT intentionally).

If you see SYN but no SYN-ACK: return routing or firewall on the far side. If you see SYN-ACK on wg0 but client still times out, you may have MTU issues or local firewall.

Third: check the service is reachable and listening where you think it is

  • Port reachability: nc -vz host 22 / nc -vz host 3389.
  • On the target: confirm listening address (ss -lntp on Linux; Windows firewall/service state on Windows).
  • Validate host firewall rules allow traffic from VPN CIDR.

Fourth: isolate performance bottlenecks (latency, MTU, DNS)

  • Latency: ping or TCP connect time, then traceroute.
  • MTU/MSS: symptoms are “connects but freezes” or “RDP black screen.”
  • DNS: if hostname fails but IP works, fix split-DNS or push internal resolvers.

Common mistakes: symptom → root cause → fix

1) “VPN connects, but I can’t reach anything internal.”

Symptom: VPN shows connected; you can ping VPN gateway but not internal hosts.

Root cause: Missing routes (AllowedIPs too narrow, or internal routers don’t know the VPN subnet).

Fix: Add routes for internal subnets to the VPN client config and add a return route on internal routers (preferred) or NAT on the gateway (temporary).

2) “SSH works to some hosts but not others, randomly.”

Symptom: Some subnets work, others timeout; it feels inconsistent.

Root cause: Overlapping IP ranges (VPN pool collides with internal ranges) or asymmetric routing through multiple exits.

Fix: Re-address the VPN pool to non-overlapping space; ensure internal routing symmetry or use policy routing on the gateway.

3) “RDP connects then black screen / hangs.”

Symptom: TCP 3389 is reachable, but the session stalls.

Root cause: MTU/MSS issues over the tunnel, or aggressive security inspection mangling traffic.

Fix: Set tunnel MTU conservatively; clamp MSS on the gateway; avoid TCP-over-TCP tunneling unless you must.

4) “We exposed only the VPN port; still got compromised.”

Symptom: The only public port is VPN, yet attacker got in.

Root cause: Weak VPN auth (no MFA), leaked keys, shared accounts, or a compromised endpoint with valid credentials.

Fix: Enforce MFA, rotate keys/certs, revoke peers quickly, require managed devices, and restrict what VPN clients can reach.

5) “DNS works for internet, but internal hostnames fail on VPN.”

Symptom: You can SSH by IP, but not by hostname; internal names don’t resolve.

Root cause: Split-DNS not configured; VPN doesn’t push internal resolver/search domains.

Fix: Push internal DNS servers and search domains via VPN config or client policy. Don’t rely on manual hosts files.

6) “Performance is terrible only on VPN.”

Symptom: High latency, choppy RDP, slow SCP.

Root cause: Hairpin routing (all traffic backhauls through HQ), overloaded gateway CPU, wrong crypto offload expectations, or packet loss on UDP path.

Fix: Use split tunneling cautiously for non-corporate traffic, add regional gateways, monitor gateway CPU, and validate packet loss with tcpdump counters.

7) “After we ‘hardened’ SSH, automation broke.”

Symptom: CI/CD jobs fail to SSH after security change.

Root cause: Disabled password auth without provisioning keys, or restricted users/groups too broadly.

Fix: Move automation to dedicated service accounts with scoped keys; test changes in a staging environment that mirrors production auth.

8) “Vendor access keeps becoming permanent.”

Symptom: Old vendor VPN accounts still work months later.

Root cause: No offboarding workflow; access is granted by exception and never reviewed.

Fix: Time-box vendor accounts, enforce periodic access reviews, and require ticket references in account metadata.

Three corporate mini-stories from the trenches

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

A mid-sized company moved a legacy accounting app into a hosted environment. The ops team was proud: no public RDP, no public SSH, just a VPN. They did the right thing—mostly. Then payroll week arrived, and RDP sessions started dropping every few minutes.

The assumption was simple and wrong: “If the VPN is connected, the network path is fine.” The team chased Windows updates, RDP settings, and user laptops. Someone even suggested increasing RDP timeouts, which is a nice way to waste time with confidence.

The real issue was return routing. The VPN gateway could reach the servers. The servers could reply, but their default gateway sent traffic to a different router that had no idea where the VPN subnet lived. Half the traffic took a scenic route into a black hole. It looked like “random disconnects” because it was.

Fixing it was boring: add a route for the VPN subnet on the correct internal router. The RDP drops vanished immediately. The postmortem note was short: never assume “connected” means “routable both ways.”

Mini-story 2: The optimization that backfired

A larger organization wanted faster VPN performance. Their network team moved the VPN service to run over TCP/443 because “it gets through every firewall.” This was pitched as a reliability improvement. It was also a trap.

The first week looked fine. Then a regional office started complaining that file transfers and RDP were laggy. Not just slow—sticky, bursty, and unpredictable. Packet captures showed retransmissions and head-of-line blocking.

They had built TCP-over-TCP in the path: a TCP VPN tunnel carrying TCP applications. When there’s loss, TCP tries to recover; your inner TCP tries to recover too. The recovery mechanisms fight, and your users get a slideshow.

The fix was to move back to UDP for the VPN where possible and reserve TCP/443 as a last resort. Performance stabilized, and the “optimization” was quietly removed from the architecture diagram.

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

A regulated company had a strict rule: every VPN user belongs to a group, every group maps to explicit subnet access, and every access change requires a ticket. Everyone hated it during calm times because it was friction. The security team was used to being unpopular. They found it relaxing.

One afternoon, an engineer’s laptop was stolen from a car. The laptop was encrypted, but the incident response team assumed worst-case: device might be compromised eventually. The question became: “What can this device reach right now?”

Because the access model was boring and explicit, the answer was immediate. The engineer’s VPN profile only routed to a jump host subnet, not production databases. Revoking access was a one-line peer removal, and the jump host required MFA and short-lived credentials anyway.

They rotated relevant keys, reviewed logs, and moved on without a week-long panic parade. The rule everyone complained about ended up being the reason the incident was a ticket, not a headline.

Joke #2: The only thing more persistent than attackers is an intern who learned port forwarding yesterday.

Checklists / step-by-step plan

Plan A: The sane default (VPN + routed access + restricted subnets)

  1. Pick a non-overlapping VPN CIDR (example: 10.42.0.0/24). Write it down. Treat it like an API contract.
  2. Deploy VPN gateway(s) with firewall default deny. Only expose the VPN port publicly.
  3. Enable MFA for VPN access. If your VPN product doesn’t support it, that’s your change request.
  4. Define role-based access: admins can reach management subnets; support can reach limited endpoints; vendors get time-boxed access to specific hosts.
  5. Push internal DNS to VPN clients and configure split-DNS if needed.
  6. Add return routes on internal routers for the VPN CIDR via the VPN gateway. Avoid NAT unless you must.
  7. Bind SSH/RDP to internal interfaces and lock host firewalls to allow only VPN CIDR or jump hosts.
  8. Centralize logs: VPN auth logs, Windows security logs, SSH logs. Set alerts for suspicious patterns.
  9. Test from a clean client: new VPN profile, verify routing, verify port reachability, verify authentication works.
  10. Document the “break glass” procedure: who can get emergency access, how it’s approved, and how it’s revoked.

Plan B: Add a jump host when you need tighter control

  1. Users VPN in, but can only reach a jump host subnet.
  2. Jump host enforces MFA/SSO again, with strict logging and session recording where feasible.
  3. From jump host, allow SSH/RDP to targets based on group membership.
  4. Disallow direct client-to-production routes. No exceptions “just for me.” Exceptions become culture.

Plan C: Vendor access without chaos

  1. Create vendor-specific VPN profiles with time limits and narrow AllowedIPs.
  2. Force access through a jump host with recorded sessions.
  3. Disable split tunneling for vendor profiles if you can; at least block access to internal resources that aren’t needed.
  4. Review vendor access regularly; revoke on contract end automatically.

FAQ

1) Is a VPN enough, or do I still need to harden SSH/RDP?

Harden anyway. The VPN reduces exposure; it doesn’t eliminate insider risk, compromised endpoints, or lateral movement. Treat VPN as a gate, not a magic force field.

2) Should I disable split tunneling?

Default to disabling it for admin profiles. If you must enable split tunneling, restrict which internal subnets are reachable and monitor DNS carefully. Most “split tunnel” incidents are actually “split brain DNS” incidents.

3) What’s better: VPN or a bastion host?

For small teams, VPN-only is fine if subnet access is restricted. For higher assurance, use both: VPN to get into the private network, bastion/jump host to control and record what happens next.

4) Why not just change SSH to a non-standard port and call it done?

Because scanners can count to 65535. Moving ports reduces noise, not risk. If your goal is “not attacked,” hiding is not a plan; isolating is.

5) Can I keep RDP/SSH open but limit by IP allowlist?

Sometimes. It’s still fragile because home IPs change, mobile networks change, and you’ll end up allowing wider ranges than you intended. VPN gives you stable identity and a single controlled ingress.

6) What’s the single biggest cause of “VPN is up but nothing works”?

Missing return routes. The VPN side knows where to send packets, but internal networks don’t know how to reply to the VPN subnet.

7) How do I handle emergency access if the VPN is down?

Design for it explicitly: out-of-band console, separate management plane, or a second VPN gateway/provider. Don’t keep public SSH/RDP “just in case.” That’s how “just in case” becomes “just got owned.”

8) Is WireGuard safe for enterprise use?

Yes, if you manage keys and access properly. Its simplicity is an advantage operationally. The enterprise question is usually about lifecycle: onboarding, offboarding, auditing, and policy enforcement.

9) What about storage/admin networks—any special concerns?

Yes: management access to storage systems is high impact. Keep storage management interfaces on dedicated subnets reachable only via VPN and ideally via a jump host. Audit aggressively.

10) How do I prove we’re no longer exposing RDP/SSH?

Run external scans from outside your network perimeter and verify ports 22 and 3389 are closed. Internally, confirm services bind to private interfaces and firewalls restrict sources to VPN CIDRs.

Next steps you can do this week

  1. Inventory exposure: identify any public-facing SSH/RDP and close it. If you can’t close it immediately, restrict by source IP and add MFA while you migrate.
  2. Stand up a VPN pilot: choose a non-overlapping CIDR, deploy one gateway, connect two admins, route to one management subnet.
  3. Fix routing properly: add return routes on internal routers rather than relying on NAT, unless you have a documented exception.
  4. Harden targets: bind SSH/RDP to internal interfaces, enforce keys/NLA, and restrict host firewall rules to VPN CIDRs.
  5. Write the runbook: copy the Fast diagnosis playbook into your on-call docs and add the specific IP ranges and interfaces from your environment.
  6. Make revocation fast: practice removing a VPN peer/user and verifying they’re cut off. If revocation takes an hour, it’s not revocation; it’s a meeting.

If you do nothing else: stop publishing admin protocols to the internet. Put them behind a VPN with strong auth and tight routing. Your future self will send thanks via the only reliable time machine: fewer incidents.

← Previous
VPN on Ubuntu/Debian: UFW/nftables Mistakes That Lock You Out (and Fixes)
Next →
Exploit markets: when bugs cost more than cars

Leave a comment