The Real Problem: How Does Your Team Handle Remote Access?
After auditing security across 10+ servers on various projects, I noticed most of them shared a common problem: no VPN at all, or a VPN set up as a “temporary” fix. Specifically:
- SSH port 22 exposed directly to the internet, immediately visible to bots
- Using OpenVPN but with a messy setup, requiring end users to copy around
.ovpnfiles - Some teams using TeamViewer or ngrok — “temporary” solutions that became permanent
The outcome was predictable: bots scanning ports around the clock, SSH brute-force attacks at 3 AM, server logs flooded with garbage. I once checked the auth.log on a staging server — within 24 hours there were 15,000 login attempts from Chinese and Russian IPs. That server only ran a dev environment, but a compromise was still an unnecessary headache.
The answer was fairly obvious: lock down all public ports and only allow access through VPN. But which VPN is easy to use, fast, and doesn’t require an entire afternoon to configure?
Why OpenVPN Isn’t Always the Answer
I’m not saying OpenVPN is bad — it’s battle-tested, stable, and I’ve used it for years. But there are a few things that make me hesitant when setting up something new:
- Slower than necessary: OpenVPN runs in userspace, so every packet has to travel kernel → userspace → kernel. Real-world benchmarks show OpenVPN typically tops out at 100–200Mbps, while WireGuard can saturate a 1Gbps link on the same hardware. That difference is very noticeable when working over VPN with a database or file server.
- Complex configuration: PKI infrastructure, CA, certificates,
.ovpnfiles — every time you add a new client it’s a whole process. - Hard to debug: When something goes wrong, OpenVPN’s logs often don’t tell you much.
If you already have a working OpenVPN infrastructure, there’s no need to switch. But for a fresh setup, WireGuard is what I’d recommend without hesitation.
WireGuard: Next-Generation VPN with a Surprisingly Small Codebase
Jason Donenfeld wrote WireGuard in just around 4,000 lines of code — compared to OpenVPN’s ~70,000 lines. Less code = smaller attack surface = easier to audit. Linus Torvalds called it “a work of art” and merged it directly into the Linux kernel starting with version 5.6 — meaning Ubuntu 20.04 and later have it built in, no additional modules required.
Here’s why I use WireGuard for every new setup:
- Runs in the kernel: No userspace overhead like OpenVPN, significantly higher throughput.
- Lightning-fast handshake: Around 100ms to establish a connection — OpenVPN typically takes 1–2 seconds.
- Modern cryptography: ChaCha20, Poly1305, Curve25519 — not the outdated RSA/AES stack.
- Minimal configuration: Each peer only needs a public key — no CA, no certificates.
- Stealth by default: WireGuard doesn’t respond to invalid packets, so port scanners will see this port as closed.
Step-by-Step WireGuard Installation and Configuration
Setup model: 1 Ubuntu Server as the VPN gateway (with a public IP) + N clients (dev laptops, office machines, etc.). Test environment: Ubuntu 22.04 LTS.
Step 1: Install WireGuard
sudo apt update
sudo apt install wireguard -y
On Ubuntu 20.04+, WireGuard is available in the official repository — no PPA needed.
Step 2: Generate Key Pair for the Server
WireGuard uses public/private keys — no usernames or passwords required. Each peer (server or client) has its own key pair.
sudo mkdir -p /etc/wireguard
cd /etc/wireguard
# Generate private key and derive public key from it
wg genkey | sudo tee server_private.key | wg pubkey | sudo tee server_public.key
# View key contents
sudo cat server_private.key
sudo cat server_public.key
Important: Never share your private key with anyone. The public key can be freely shared — that’s the principle of asymmetric cryptography.
Step 3: Create the Server Configuration File
sudo nano /etc/wireguard/wg0.conf
Configuration file content:
[Interface]
# Paste the contents of server_private.key here
PrivateKey = <SERVER_PRIVATE_KEY>
# Tunnel interface IP (internal VPN IP range, not the public IP)
Address = 10.0.0.1/24
# Port WireGuard listens on (UDP)
ListenPort = 51820
# Enable IP forwarding and NAT when the tunnel comes up
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# Add each client as a separate [Peer] block
[Peer]
# Client 1's public key
PublicKey = <CLIENT1_PUBLIC_KEY>
# IP assigned to this client in the tunnel
AllowedIPs = 10.0.0.2/32
Note: Replace eth0 with your server’s actual network interface. Run ip a to confirm — depending on your VPS provider it could be ens3, enp0s3, or eth0. Don’t guess — always verify.
Step 4: Enable IP Forwarding
# Enable immediately
sudo sysctl -w net.ipv4.ip_forward=1
# Enable permanently (persists across reboots)
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Step 5: Generate Key Pair for the Client
On a Linux client machine:
sudo apt install wireguard -y
wg genkey | tee client_private.key | wg pubkey | tee client_public.key
If the client is Windows or macOS, use the official WireGuard app — it has a “Generate keypair” button in the UI, no terminal required.
Step 6: Configure the Client
Create this config file on the client (or import it into the WireGuard app on Windows/macOS):
[Interface]
# Paste the contents of client_private.key here
PrivateKey = <CLIENT_PRIVATE_KEY>
# IP assigned to the client in the VPN tunnel
Address = 10.0.0.2/24
# DNS when connected to VPN (optional)
DNS = 1.1.1.1
[Peer]
# SERVER's public key
PublicKey = <SERVER_PUBLIC_KEY>
# Server's public IP + port
Endpoint = <SERVER_PUBLIC_IP>:51820
# Which traffic routes through VPN:
# 0.0.0.0/0 = all traffic (full tunnel)
# 10.0.0.0/24 = only internal VPN traffic (split tunnel)
AllowedIPs = 10.0.0.0/24
# Maintain connection when behind NAT (home router, 4G, etc.)
PersistentKeepalive = 25
Step 7: Start WireGuard on the Server
# Start the tunnel
sudo wg-quick up wg0
# Enable auto-start on reboot
sudo systemctl enable wg-quick@wg0
# Check status
sudo wg show
The output of wg show displays tunnel information and the list of peers. Seeing a latest handshake line in the peer section means the client has connected successfully.
Step 8: Open the Port on the Server Firewall
# If using UFW
sudo ufw allow 51820/udp
sudo ufw reload
# If using iptables directly
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
Testing the Connection
Once the client is connected, run a quick test from the terminal:
# Ping the server via VPN IP (not the public IP)
ping 10.0.0.1
# SSH into the server via VPN IP
ssh [email protected]
If ping and SSH work through 10.0.0.1, the VPN is working correctly. The handshake typically completes in under a second — the first time you connect, you’ll be surprised by how fast it is. From here you can close SSH port 22 on the firewall — only users with a WireGuard key can reach the server.
Adding New Clients: No Service Restart Required
This is my favorite thing about WireGuard. Adding a new client requires no certificate signing and no tunnel restart:
# Add a new peer immediately (takes effect without restart)
sudo wg set wg0 peer <CLIENT2_PUBLIC_KEY> allowed-ips 10.0.0.3/32
# Also add to /etc/wireguard/wg0.conf to persist across reboots:
# [Peer]
# PublicKey = <CLIENT2_PUBLIC_KEY>
# AllowedIPs = 10.0.0.3/32
Troubleshooting Common Issues
- Can ping 10.0.0.1 but no internet access: Run
cat /proc/sys/net/ipv4/ip_forward— it must return1. Already correct but still broken? Check that the interface name in PostUp matches the output ofip a. - Can’t connect at all: Run
sudo wg show— if there’s nolatest handshake, UDP port 51820 is being blocked. Check your UFW rules or VPS Security Group settings. - Connection drops after a few minutes: Add
PersistentKeepalive = 25to the client config — this is required when the client is behind NAT. - “invalid key” error: The config file must contain the actual key content (a base64 string), not the filename
server_private.key.
Once set up for the whole team, the server logs were spotless. No more SSH brute-force, no more thousands of failed login attempts every day. The server has almost all public ports closed, with only 51820/UDP and 443/TCP open — exactly the security posture I wanted from the start.
