Overview
WireGuard is a modern, kernel-level VPN that replaces legacy solutions like OpenVPN and IPsec with a dramatically simpler design. A single UDP port, ~4,000 lines of auditable code, and ChaCha20-Poly1305 encryption give you a fast, low-latency tunnel that is easy to reason about and harden.
This project walks through the "road warrior" pattern — one always-on server, many roaming clients. You will end up with:
- A fully routed WireGuard server that forwards all client traffic (full-tunnel mode)
- Per-client key pairs provisioned as terminal QR codes for instant mobile import
- DNS leak protection via a VPN-side resolver
- A UFW-based firewall with a kill-switch profile
- Systemd auto-start with
wg-quick
Target platform is Ubuntu Server 22.04 / 24.04 (works on any Debian-based distro). The server can be a VPS, a home server behind a static IP, or a homelab VM exposed via port forwarding.
Architecture
[Laptop / Phone] [Ubuntu VPN Server] [Internet]
wg0: 10.10.0.2/32 ──UDP──► wg0: 10.10.0.1/24 ──NAT──► 8.8.8.8
wg0: 10.10.0.3/32 ──UDP──► eth0: <public IP>
All client traffic is encapsulated in WireGuard UDP datagrams aimed at port 51820 on the server. The server uses iptables MASQUERADE to NAT that traffic out through its WAN interface (eth0 / ens3 — varies by host). From the internet's perspective, all client requests appear to originate from the server's public IP.
Key design decisions:
| Decision | Choice | Why |
|---|---|---|
| Routing mode | Full tunnel (AllowedIPs = 0.0.0.0/0) | Prevent traffic leaks outside VPN |
| DNS | Cloudflare 1.1.1.1 via VPN interface | Stop DNS leaks to local ISP |
| Key auth | Per-client Curve25519 + optional PSK | Post-quantum forward secrecy |
| Firewall | UFW + iptables PostUp/PostDown | Auto-clean on interface down |
| Provisioning | QR codes via qrencode | 10-second mobile onboarding |
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IPv4 address (VPS or home server + port forwarding)
- Root or sudo access
- Port
51820/UDPopen inbound in your cloud provider security group or home router - A domain or static IP you can hand to clients
Check your WAN interface name — it varies by provider:
ip -o -4 route show to default | awk '{print $5}'
# common values: eth0, ens3, enp3s0, ens160Save the result — you will use it in the PostUp/PostDown rules.
Step 1 — Install WireGuard
sudo apt update && sudo apt upgrade -y
sudo apt install -y wireguard wireguard-tools qrencodeWireGuard is in the mainline kernel since 5.6, so no DKMS module is needed on Ubuntu 22.04+. Verify the module loads:
sudo modprobe wireguard && echo "wireguard module OK"Step 2 — Generate Server Keys
Work inside /etc/wireguard/ and lock down permissions immediately:
cd /etc/wireguard
sudo umask 077
# Generate private key, derive public key
sudo wg genkey | sudo tee server_private.key | sudo wg pubkey | sudo tee server_public.key
# Verify
sudo cat server_public.key # base64 string ~44 charsKeep server_private.key secret — it never leaves the server.
Step 3 — Configure the Server Interface
Create /etc/wireguard/wg0.conf. Replace <SERVER_PRIVATE_KEY> with the contents of server_private.key and WAN_IFACE with your interface name (e.g. eth0):
[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.10.0.1/24
ListenPort = 51820
DNS = 1.1.1.1
# NAT masquerading — swap eth0 for your WAN interface
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
# Peers are added below — one [Peer] block per clientLock down the config file:
sudo chmod 600 /etc/wireguard/wg0.conf
sudo chmod 600 /etc/wireguard/server_private.keyStep 4 — Enable IP Forwarding
WireGuard forwards packets between the VPN subnet and the internet. IP forwarding must be enabled at the kernel level:
# Persist across reboots
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -pVerify:
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1Step 5 — Configure UFW Firewall
Allow WireGuard and SSH before enabling the firewall:
sudo ufw allow 22/tcp # SSH — do NOT skip this step
sudo ufw allow 51820/udp # WireGuard
# UFW's DEFAULT_FORWARD_POLICY blocks forwarded packets
sudo sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' \
/etc/default/ufw
sudo ufw enable
sudo ufw status verboseStep 6 — Start the WireGuard Service
sudo systemctl enable --now wg-quick@wg0
# Confirm it is running
sudo systemctl status wg-quick@wg0
sudo wg showwg show should display the wg0 interface with your server public key and listening port. No peers yet — that comes next.
Step 7 — Add Client Peers
Repeat this block for every device you want to connect. Replace <N> with an incrementing number starting at 2 (e.g. 10.10.0.2, 10.10.0.3, …).
7a — Generate client keys on the server
cd /etc/wireguard
sudo wg genkey | sudo tee client<N>_private.key | sudo wg pubkey | sudo tee client<N>_public.key
# Optional: pre-shared key for extra layer of symmetric encryption
sudo wg genpsk | sudo tee client<N>_psk.key7b — Append a [Peer] block to wg0.conf
[Peer]
# Client N — <device description>
PublicKey = <CLIENT_N_PUBLIC_KEY>
PresharedKey = <CLIENT_N_PSK> # remove line if not using PSK
AllowedIPs = 10.10.0.<N>/32Hot-reload the config without dropping existing sessions:
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)7c — Build the client config file
Create /etc/wireguard/client<N>.conf:
[Interface]
PrivateKey = <CLIENT_N_PRIVATE_KEY>
Address = 10.10.0.<N>/32
DNS = 1.1.1.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <CLIENT_N_PSK>
Endpoint = <SERVER_PUBLIC_IP>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25AllowedIPs = 0.0.0.0/0, ::/0 routes all IPv4 and IPv6 traffic through the VPN (full-tunnel / kill-switch behaviour on most WireGuard clients).
PersistentKeepalive = 25 keeps the NAT mapping alive when the client is behind a firewall that closes idle UDP sessions.
Step 8 — Provision Clients via QR Code
For iOS and Android, the WireGuard app can import a config by scanning a QR code — no file transfer needed:
# Display QR in the terminal (scan with WireGuard app)
sudo qrencode -t ansiutf8 < /etc/wireguard/client<N>.confFor desktop clients:
- Linux: copy
client<N>.confto/etc/wireguard/and runsudo wg-quick up client<N> - Windows/macOS: import via the WireGuard GUI (File → Import tunnel from file)
- Android/iOS: scan the QR code in the WireGuard app
Testing
From the client
# Connect (Linux)
sudo wg-quick up client<N>
# Verify your public IP changed
curl https://ifconfig.me
# Ping the server's VPN IP
ping 10.10.0.1
# Check for DNS leaks
# Visit https://dnsleaktest.com — all resolvers should be Cloudflare or your chosen DNSFrom the server
# See connected peers, handshake timestamps, data transfer
sudo wg show
# Sample output:
# peer: <client public key>
# endpoint: <client IP>:<port>
# allowed ips: 10.10.0.2/32
# latest handshake: 12 seconds ago
# transfer: 1.23 MiB received, 456 KiB sentA "latest handshake" within the last few minutes confirms the tunnel is active and data is flowing.
Kill-switch test
- Connect the WireGuard client with
AllowedIPs = 0.0.0.0/0. - While connected, disable the WireGuard interface on the client (
sudo wg-quick down client<N>). - Attempt to reach the internet — most WireGuard clients will block traffic until the tunnel is restored.
Hardening Checklist
# 1. Rotate keys if a device is lost
# Remove the [Peer] block from wg0.conf and sync:
sudo wg set wg0 peer <OLD_PUBLIC_KEY> remove
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
# Then delete the old key files
# 2. Restrict SSH to VPN subnet only (optional, advanced)
sudo ufw delete allow 22/tcp
sudo ufw allow from 10.10.0.0/24 to any port 22
# 3. Fail2ban for SSH brute-force (WireGuard itself uses PKI — no brute-force surface)
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
# 4. Automatic security updates
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgradesDeployment
This setup runs cleanly on any of the following:
| Platform | Notes |
|---|---|
| VPS (Hetzner, DigitalOcean, Vultr) | Open port 51820/UDP in provider firewall |
| Home server (behind router) | Port forward 51820/UDP → server LAN IP |
| Proxmox VM | Use virtio NIC; enable IP forwarding in VM |
| LXC container | Privileged container required; add lxc.cap.drop = workaround |
| Raspberry Pi 4 | Excellent low-power option; Ubuntu Server 22.04 arm64 |
For a VPS deployment, a $5/month Hetzner CX11 or DigitalOcean Droplet handles dozens of concurrent clients without breaking a sweat — WireGuard's kernel-space implementation is extremely efficient.
Extensions and Next Steps
Multi-server mesh (site-to-site): Add a [Peer] block on each server pointing to the other, with AllowedIPs set to the remote LAN subnet. This turns your road-warrior setup into a full mesh between your home network and a remote datacenter.
Internal DNS with AdGuard Home or Pi-hole: Replace DNS = 1.1.1.1 with the VPN IP of a Pi-hole container (10.10.0.100). All VPN clients get ad-blocking at the DNS layer with zero client config changes.
WireGuard UI (wg-easy): A Docker-based web UI that manages peers, QR codes, and bandwidth graphs without touching config files:
docker run -d \
--name wg-easy \
-e WG_HOST=<your-server-ip> \
-e PASSWORD=<admin-password> \
-v ~/.wg-easy:/etc/wireguard \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--restart unless-stopped \
ghcr.io/wg-easy/wg-easyMonitoring: Export WireGuard peer stats to Prometheus using prometheus-wireguard-exporter, then visualise handshake latency and per-peer bandwidth in Grafana.
IPv6 dual-stack: Add an fd10::/64 ULA subnet alongside 10.10.0.0/24 and configure Address = fd10::1/64 on the server interface. Clients receive both IPv4 and IPv6 addresses, preventing IPv6 leaks common on single-stack VPN setups.