I spent years using OpenVPN for internal VPNs and I was always mildly frustrated by it. The configuration files are long, the certificates require careful management, the performance is decent but not great, and debugging connection issues is painful. Then I tried WireGuard and everything got simpler.
WireGuard is modern, fast, and has a remarkably clean configuration. It’s been in the Linux kernel since 5.6, so on any reasonably current system, it just works.
How WireGuard Is Different
WireGuard uses a different model than OpenVPN or IPsec. Instead of a client-server model with CA certificates, authentication, and negotiation, WireGuard is peer-to-peer. Each peer has a public/private key pair, and peers whitelist each other by exchanging public keys.
There’s no certificate management, no certificate authority, no revocation — just public key exchange. It’s similar in concept to SSH authorized_keys.
The protocol is UDP-only. If UDP is blocked on your network, WireGuard won’t work. In most cases this isn’t a problem.
Installing WireGuard
# Ubuntu 20.04+
apt install wireguard
# Debian 11+
apt install wireguard
# CentOS/RHEL 8+
dnf install wireguard-tools
On Ubuntu 18.04 you need the backport:
add-apt-repository ppa:wireguard/wireguard
apt install wireguard
Generating Keys
For each peer (server and each client):
wg genkey | tee privatekey | wg pubkey > publickey
chmod 600 privatekey
Store private keys securely — if you share a private key, anyone who has it can impersonate that peer. Public keys are meant to be shared.
Server Configuration
I’ll set up a server at 10.0.0.1/24 listening on UDP 51820. Edit /etc/wireguard/wg0.conf:
[Interface]
# The server's private key
PrivateKey = <server-private-key>
# The server's VPN IP
Address = 10.0.0.1/24
# UDP port WireGuard listens on
ListenPort = 51820
# Enable IP forwarding when the interface comes up
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# One section per client peer
[Peer]
# Client's public key
PublicKey = <client-public-key>
# This client gets 10.0.0.2
AllowedIPs = 10.0.0.2/32
[Peer]
PublicKey = <client2-public-key>
AllowedIPs = 10.0.0.3/32
Replace eth0 in PostUp/PostDown with your actual network interface (ip addr to check).
Enable IP forwarding permanently:
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf
Open the firewall port:
ufw allow 51820/udp
Start and enable:
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
Or manually:
wg-quick up wg0
wg-quick down wg0
Client Configuration
On the client machine, create /etc/wireguard/wg0.conf:
[Interface]
# Client's private key
PrivateKey = <client-private-key>
# Client's VPN IP (must match AllowedIPs on server for this client)
Address = 10.0.0.2/24
# Optional: use the VPN's DNS
DNS = 10.0.0.1
[Peer]
# Server's public key
PublicKey = <server-public-key>
# Server's public IP and port
Endpoint = server.example.com:51820
# Route all traffic through VPN, or just specific subnets
# All traffic:
AllowedIPs = 0.0.0.0/0, ::/0
# Just VPN subnet:
# AllowedIPs = 10.0.0.0/24
# Keep-alive for NAT traversal (useful behind NAT)
PersistentKeepalive = 25
Start it:
wg-quick up wg0
Checking Status
wg show
Output shows each peer, their public key, endpoint, allowed IPs, and the last handshake time. If a peer shows “never” for last handshake, the connection isn’t established.
wg showconf wg0 # Shows the running config
Ping through the VPN to test:
ping 10.0.0.1 # From client, ping server VPN IP
Split Tunneling vs. Full Tunnel
If AllowedIPs = 0.0.0.0/0 on the client, all traffic routes through the VPN. This is a “full tunnel”.
For split tunneling (only route specific networks through VPN):
AllowedIPs = 10.0.0.0/24, 192.168.100.0/24
Only traffic destined for those subnets goes through WireGuard. Everything else goes through the normal internet connection. This is useful for corporate setups where you need access to internal resources but want to keep general browsing local.
Adding and Removing Peers
You don’t need to restart the interface to add peers. On a running interface:
# Add a peer
wg set wg0 peer <public-key> allowed-ips 10.0.0.10/32
# Remove a peer
wg set wg0 peer <public-key> remove
To make changes persistent, edit wg0.conf and reload:
wg addconf wg0 <(wg-quick strip wg0)
Or just wg-quick down wg0 && wg-quick up wg0 if a brief interruption is acceptable.
Key Rotation
There’s no automatic key rotation built into WireGuard. When I rotate keys, I:
- Generate new key pair on the client
- Update the server config with the new public key
- Remove the old peer from the server
- Update the client config with the new private key
The old peer entry on the server should be removed promptly — it won’t connect anymore once the client has a new key, but it’s cleaner to remove it.
Preshared Keys (Optional Extra Security)
WireGuard supports an optional preshared key per peer pair. This adds a layer of symmetric encryption on top of the asymmetric key exchange:
# Generate preshared key
wg genpsk > preshared.key
Add to both server and client configs under [Peer]:
[Peer]
PublicKey = ...
PresharedKey = <preshared-key>
AllowedIPs = 10.0.0.2/32
This provides post-quantum resistance — even if asymmetric encryption is broken in the future, the symmetric preshared key would need to be compromised too. For most setups it’s optional, but it’s a good practice for sensitive infrastructure.
Performance
WireGuard is fast. On a modern server, it can push multi-gigabit throughput because the crypto (ChaCha20-Poly1305 and Curve25519) is efficient and the kernel module avoids userspace overhead.
For comparison: OpenVPN typically saturates at a few hundred Mbps on a single core. WireGuard can push 1–2+ Gbps depending on hardware.
For site-to-site VPN links or connecting remote offices, this difference actually matters. For personal VPN use, it’s fast enough that you don’t notice the VPN overhead at all.
Troubleshooting
No handshake — check that the server’s UDP port 51820 is reachable from the client. Check firewall rules on both sides. Check that the public keys match (easy to mix up).
Connected but can’t route — verify IP forwarding is enabled on the server (cat /proc/sys/net/ipv4/ip_forward should return 1). Check that the iptables masquerade rule in PostUp ran correctly (iptables -t nat -L -n -v).
DNS not resolving through VPN — if you set DNS in the client config, wg-quick should configure it. Check resolv.conf or use resolvctl status to see current DNS settings.
Dropped connection on mobile — add PersistentKeepalive = 25 on the client. Mobile networks kill UDP connections if no traffic flows for a while; keepalive packets prevent this.
WireGuard has become my go-to for everything from personal VPNs to production site-to-site links. It’s simple enough that I understand exactly what’s happening, and reliable enough that I don’t think about it once it’s running.