Introduction
WireGuard is a modern VPN protocol built directly into the Linux kernel. Compared to OpenVPN or IPSec, it offers a significantly smaller codebase (~4,000 lines vs. hundreds of thousands), faster handshakes, and a simpler configuration model — all while using state-of-the-art cryptography (ChaCha20, Poly1305, Curve25519, BLAKE2s, and SipHash).
This guide walks through deploying a WireGuard server on Ubuntu/Debian, configuring one or more clients, setting up proper firewall rules with nftables, and applying hardening measures suited for production or homelab environments. By the end you will have an encrypted tunnel that routes all client traffic through the server, with key rotation procedures and monitoring in place.
Prerequisites
Before starting, confirm the following:
- Server OS: Ubuntu 22.04 LTS or Debian 12 (WireGuard ships in the mainline kernel from 5.6+; Ubuntu 20.04 requires a manual kernel module install)
- Server resources: 1 vCPU / 512 MB RAM is sufficient for a personal or small-team VPN
- Open port: UDP 51820 (or your chosen port) reachable from the internet
- IP forwarding: must be enabled on the server (covered in Step 3)
- Clients: WireGuard apps are available at wireguard.com/install/ for all major platforms
Step 1 — Install WireGuard
On Ubuntu 22.04 or Debian 12, WireGuard is available from the default repositories:
sudo apt update && sudo apt install -y wireguard wireguard-toolsVerify the kernel module loaded:
sudo modprobe wireguard
lsmod | grep wireguard
# Expected output:
# wireguard 102400 0If the module is missing on older kernels, add the WireGuard PPA first:
sudo add-apt-repository ppa:wireguard/wireguard
sudo apt update && sudo apt install -y wireguardStep 2 — Generate Server and Client Key Pairs
WireGuard uses Curve25519 asymmetric keys. Each peer (server or client) needs a private/public key pair. Keep private keys strictly confidential — they never leave their respective host.
# Create a secure working directory
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
cd /etc/wireguard
# Generate server keys
wg genkey | sudo tee server_private.key | wg pubkey | sudo tee server_public.key
sudo chmod 600 server_private.key
# Generate keys for client 1 (repeat for each additional client)
wg genkey | sudo tee client1_private.key | wg pubkey | sudo tee client1_public.key
sudo chmod 600 client1_private.key
# Generate a pre-shared key for extra forward secrecy (optional but recommended)
wg genpsk | sudo tee client1_preshared.key
sudo chmod 600 client1_preshared.keyPrint the keys you will need for configuration:
echo "Server private: $(sudo cat /etc/wireguard/server_private.key)"
echo "Server public: $(sudo cat /etc/wireguard/server_public.key)"
echo "Client1 private: $(sudo cat /etc/wireguard/client1_private.key)"
echo "Client1 public: $(sudo cat /etc/wireguard/client1_public.key)"
echo "Client1 PSK: $(sudo cat /etc/wireguard/client1_preshared.key)"Step 3 — Enable IP Forwarding
The server must forward packets between the tunnel interface and the internet-facing interface.
# Enable immediately (non-persistent)
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
# Make persistent across reboots
sudo tee -a /etc/sysctl.d/99-wireguard.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# Harden: disable source routing and ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
EOF
sudo sysctl --systemStep 4 — Configure the WireGuard Server
Determine your server's outbound interface name (typically eth0 or ens3):
ip route | grep default
# Example: default via 203.0.113.1 dev eth0 proto staticCreate the server configuration file. Replace SERVER_PRIVATE_KEY and CLIENT1_PUBLIC_KEY with the actual key values from Step 2, and adjust the interface name in the PostUp/PostDown rules to match your server.
sudo tee /etc/wireguard/wg0.conf <<EOF
[Interface]
# Server private key
PrivateKey = <SERVER_PRIVATE_KEY>
# VPN subnet — server takes .1, clients get .2, .3, etc.
Address = 10.8.0.1/24
# UDP port WireGuard listens on
ListenPort = 51820
# Save peer changes made with 'wg set' back to this file
SaveConfig = false
# NAT: masquerade VPN traffic as the server's public IP
PostUp = nft add table ip wireguard; \
nft add chain ip wireguard postrouting { type nat hook postrouting priority 100 \; }; \
nft add rule ip wireguard postrouting oifname "eth0" masquerade
PostDown = nft delete table ip wireguard
# Drop martian packets on the tunnel interface
PostUp = nft add table ip wg_filter; \
nft add chain ip wg_filter input { type filter hook input priority 0 \; policy drop \; }; \
nft add rule ip wg_filter input iifname "wg0" accept; \
nft add rule ip wg_filter input ct state established,related accept; \
nft add rule ip wg_filter input iifname "lo" accept
PostDown = nft delete table ip wg_filter
# --- Peers ---
[Peer]
# Client 1 — laptop
PublicKey = <CLIENT1_PUBLIC_KEY>
PresharedKey = <CLIENT1_PRESHARED_KEY>
# IP address assigned to this client inside the VPN
AllowedIPs = 10.8.0.2/32
EOF
sudo chmod 600 /etc/wireguard/wg0.confNote on
SaveConfig: SettingSaveConfig = falseprevents WireGuard from overwriting your hand-crafted config when the interface goes down. If you add peers dynamically withwg set, set it totrueinstead.
Step 5 — Configure the Firewall
Open the WireGuard UDP port using ufw or directly with nftables. The PostUp rules in the server config already handle NAT, but you still need to allow the inbound port:
Option A — ufw (simpler)
sudo ufw allow 51820/udp
sudo ufw allow OpenSSH # ensure SSH remains open
sudo ufw enable
sudo ufw statusOption B — nftables (production-grade)
sudo tee /etc/nftables.conf <<'NFTEOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iifname "lo" accept
ip protocol icmp accept
tcp dport 22 accept # SSH
udp dport 51820 accept # WireGuard
iifname "wg0" accept # allow traffic from VPN peers
}
chain forward {
type filter hook forward priority 0; policy drop;
iifname "wg0" oifname "eth0" accept # VPN → internet
iifname "eth0" oifname "wg0" ct state established,related accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
NFTEOF
sudo systemctl enable nftables
sudo systemctl restart nftablesStep 6 — Start WireGuard and Enable on Boot
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0Verify the interface is up:
sudo wg show
# Expected output:
# interface: wg0
# public key: <server public key>
# private key: (hidden)
# listening port: 51820
#
# peer: <client1 public key>
# preshared key: (hidden)
# allowed ips: 10.8.0.2/32Step 7 — Configure the Client
Linux Client
Install WireGuard on the client machine, then create its configuration:
sudo tee /etc/wireguard/wg0.conf <<EOF
[Interface]
PrivateKey = <CLIENT1_PRIVATE_KEY>
Address = 10.8.0.2/32
DNS = 1.1.1.1, 8.8.8.8
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <CLIENT1_PRESHARED_KEY>
Endpoint = <SERVER_PUBLIC_IP_OR_HOSTNAME>:51820
# Route ALL traffic through VPN; change to 10.8.0.0/24 for split-tunnel
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
sudo wg-quick up wg0Mobile / GUI Clients (iOS, Android, Windows, macOS)
The easiest method is to generate a QR code from the client config on the server:
# Install qrencode
sudo apt install -y qrencode
# Generate QR code (display in terminal)
qrencode -t ansiutf8 < /etc/wireguard/client1.confCreate /etc/wireguard/client1.conf using the same [Interface] + [Peer] structure above, then encode it. Scan the QR from the official WireGuard app.
Step 8 — Security Hardening
8.1 Restrict Config File Permissions
sudo chmod 600 /etc/wireguard/*.conf
sudo chmod 600 /etc/wireguard/*.key
sudo chown root:root /etc/wireguard/*.conf /etc/wireguard/*.key8.2 Use a Non-Standard Port
Avoid 51820 if your deployment is high-visibility. Edit ListenPort in wg0.conf to any unused UDP port (e.g., 4096, 59000), then update your firewall rules and client Endpoint accordingly.
8.3 Rate-Limit Handshake Attempts
Prevent brute-force amplification by rate-limiting inbound WireGuard UDP packets at the firewall:
# Add to the nftables input chain (before the allow rule)
sudo nft add rule inet filter input udp dport 51820 limit rate over 100/second drop8.4 Rotate Pre-Shared Keys Periodically
Pre-shared keys add a symmetric layer on top of the asymmetric handshake, providing post-quantum forward secrecy. Rotate them quarterly:
# Generate new PSK
NEW_PSK=$(wg genpsk)
echo "$NEW_PSK" | sudo tee /etc/wireguard/client1_preshared.key
# Update running config without a full restart
sudo wg set wg0 peer <CLIENT1_PUBLIC_KEY> preshared-key /etc/wireguard/client1_preshared.key
# Update wg0.conf for persistence
sudo sed -i "s|PresharedKey = .*|PresharedKey = $NEW_PSK|" /etc/wireguard/wg0.conf8.5 Monitor with Fail2ban (Optional)
WireGuard itself doesn't expose traditional auth logs, but you can monitor for unexpected peer connection attempts in kernel logs and react with custom Fail2ban filters targeting your firewall logs.
sudo apt install -y fail2banCreate /etc/fail2ban/filter.d/wireguard.conf:
[Definition]
failregex = nft.*wireguard.*SRC=<HOST>
ignoreregex =8.6 Disable Unused IPv6 (If Not Needed)
If you are not routing IPv6 over the tunnel, prevent IPv6 leaks on clients by removing ::/0 from AllowedIPs in the client config.
Step 9 — Verification and Testing
Verify the Tunnel from the Client
# Check the interface is up
sudo wg show
# Ping the server's VPN address
ping 10.8.0.1
# Confirm traffic routes through the VPN
curl https://ifconfig.me
# Should return the SERVER's public IP, not the client'sCheck Handshake Timestamps
A recent handshake confirms bidirectional connectivity:
sudo wg show wg0 latest-handshakes
# Output: <client_pubkey> <unix_timestamp>
# Convert: date -d @<timestamp>Verify DNS is Leaking Correctly
# On the client with 0.0.0.0/0 route
nslookup whoami.akamai.net
# Should resolve from the server-side DNS, not your local ISPUse https://dnsleaktest.com for an end-to-end DNS leak test.
Troubleshooting
Handshake Never Completes
# Check the server is listening
sudo ss -ulnp | grep 51820
# Confirm the firewall allows UDP 51820
sudo nft list ruleset | grep 51820
# Test UDP reachability from the client (requires netcat)
nc -zu <SERVER_IP> 51820 && echo "Port open" || echo "Port closed"No Internet Access After Connecting
# Confirm IP forwarding is active
sysctl net.ipv4.ip_forward # should be 1
# Confirm NAT rule is loaded
sudo nft list table ip wireguard
# Check default route on the client
ip route
# 0.0.0.0/0 should point to the wg0 interfacePeer Disconnects After Idle Period
Add PersistentKeepalive = 25 to the [Peer] block in the client config. This sends a keepalive packet every 25 seconds, preventing NAT session tables from expiring — essential when clients sit behind NAT routers.
Config Changes Not Taking Effect
WireGuard reads the config only on interface bring-up. After editing wg0.conf, restart the interface:
sudo wg-quick down wg0 && sudo wg-quick up wg0
# or
sudo systemctl restart wg-quick@wg0Key Mismatch Errors in Logs
journalctl -u wg-quick@wg0 --since "10 minutes ago"A "Invalid public key" error means the server's [Peer] PublicKey doesn't match the client's actual private key. Regenerate and copy keys carefully — base64 keys are easy to accidentally truncate.
Summary
You now have a production-ready WireGuard VPN deployment:
| Component | Configuration |
|---|---|
| Encryption | ChaCha20-Poly1305 + Curve25519 key exchange |
| Forward secrecy | Per-session ephemeral keys + optional PSK layer |
| NAT | nftables masquerade on the server's public interface |
| Firewall | nftables with explicit allow rules and rate limiting |
| Systemd | wg-quick@wg0 service, enabled on boot |
| Hardening | 600 permissions on keys, non-default port, rate limiting, PSK rotation |
WireGuard's minimal attack surface and in-kernel implementation make it one of the most defensible VPN options available today. For multi-site mesh topologies, explore tools like Netbird or Tailscale which build on WireGuard and add centralized key management and ACLs.