WireGuard Setup - Road Warrior and Site-to-Site VPN
Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 2: Services
Prerequisites: wireguard-vpn, firewalls-nftables, ip-addressing-subnets
Time: 2-3 hours
Tags: wireguard, vpn, site-to-site, road-warrior, netplan, systemd
Summary
Stand up two WireGuard patterns used across the fleet: a road-warrior server for laptops/phones to reach the LAN, and a persistent site-to-site tunnel between two locations. Covers key management, the AllowedIPs mental model, forwarding rules in nftables, and bringing interfaces up with wg-quick/systemd or netplan.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Generate and distribute keys safely
- โ Run a road-warrior hub with NAT and DNS pushed to clients
- โ Build a routed site-to-site tunnel between two subnets
- โ Diagnose the three classic failures: handshake, routing, MTU
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
Context / Why This Matters
wireguard-vpn explains what WireGuard is and why we chose it over OpenVPN/IPsec. This article is the operational how-to with fleet-specific addressing: the VPN transit network lives at 10.10.0.0/24, road-warrior clients get .x addresses from that pool, and site-to-site uses dedicated /30s from 10.10.1.0/24. All remote access funnels here instead of exposing services directly โ see network-segmentation.
Implementation / Core Content
Install and generate keys (every node)
sudo apt install wireguard
umask 077
wg genkey | tee peer1.key | wg pubkey > peer1.pub
Rule: one keypair per device, never shared. Track them in a password manager or vault (vault-secrets); treat private keys like SSH keys.
Pattern 1: Road-warrior hub
Server /etc/wireguard/wg0.conf:
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
# NAT for client traffic leaving toward LAN/internet (nftables alternative below)
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
# Optional: push split-DNS by pointing clients' DNS at the hub's dnsmasq
# (see dhcp-dns-split-horizon)
[Peer] # laptop
PublicKey = <laptop-public-key>
AllowedIPs = 10.10.0.2/32
[Peer] # phone
PublicKey = <phone-public-key>
AllowedIPs = 10.10.0.3/32
Client config:
[Interface]
PrivateKey = <laptop-private-key>
Address = 10.10.0.2/24
DNS = 192.168.20.2 # internal resolver via tunnel
[Peer] # hub
PublicKey = <server-public-key>
Endpoint = vpn.fogserv.cloud:51820
AllowedIPs = 10.10.0.0/24, 192.168.20.0/24 # split tunnel: LAN + peers only
PersistentKeepalive = 25 # needed behind NAT'd client networks
Open the port: sudo ufw allow 51820/udp or an nftables input rule.
Pattern 2: Site-to-site
Site A (LAN 192.168.20.0/24) โ Site B (LAN 192.168.30.0/24).
Site A /etc/wireguard/wg1.conf:
[Interface]
Address = 10.10.1.1/30
ListenPort = 51821
PrivateKey = <a-key>
Table = off # we manage routes explicitly
PostUp = ip route add 192.168.30.0/24 dev wg1
PostUp = sysctl -w net.ipv4.ip_forward=1
[Peer]
PublicKey = <b-pub>
Endpoint = siteb.fogserv.cloud:51821
AllowedIPs = 10.10.1.0/30, 192.168.30.0/24
PersistentKeepalive = 25
Site B mirrors it with swapped addresses/routes. Because both sides know each other's CIDRs in AllowedIPs, WireGuard performs the crypto-key routing; the explicit routes make the kernels forward.
Bringing it up
sudo systemctl enable --now wg-quick@wg0 # road warrior
sudo systemctl enable --now wg-quick@wg1 # site-to-site
# Verify
sudo wg show
interface: wg0
public key: ...
listening port: 51820
peer: ... endpoint: 203.0.113.7:51820
allowed ips: 10.10.0.2/32
latest handshake: 34 seconds ago <-- the health signal
transfer: 1.2 MiB received, 3.4 MiB sent
ping 10.10.0.2
Netplan alternative (no wg-quick)
On servers managed declaratively, netplan can own the interface:
network:
tunnels:
wg0:
mode: wireguard
addresses: [10.10.0.1/24]
port: 51820
key: /etc/wireguard/server.key
peers:
- keys:
public: <laptop-public-key>
allowed-ips: [10.10.0.2/32]
Pick one owner per interface โ don't define the same tunnel in both netplan and wg-quick.
Practical Examples
Add a new device in 60 seconds
wg genkey | tee tablet.key | wg pubkey > tablet.pub
# append [Peer] block on hub with AllowedIPs = 10.10.0.4/32
sudo systemctl reload wg-quick@wg0 || sudo wg syncconf wg0 <(wg-quick strip wg0)
Watch handshakes live while testing
watch -n2 'sudo wg show | grep -E "peer|handshake"'
Route check through tunnel
traceroute -n 192.168.20.10 # from road-warrior laptop
# hop 1 should be 10.10.0.1 (hub), then LAN IP
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| No handshake ever | Wrong Endpoint/port, UDP blocked, keys swapped | Confirm pub keys match; test UDP 51820 externally |
| Handshake OK but no traffic | Missing ip_forward, or AllowedIPs too narrow |
Enable forwarding; include remote LAN CIDR in AllowedIPs |
| Works then dies after idle | Client behind aggressive NAT | PersistentKeepalive = 25 on the client side |
| Intermittent stalls on big transfers | MTU overhead | Set MTU = 1420 in [Interface] |
| Two devices same key | Key reuse breaks cryptokey routing | One keypair per device, rotate the offender |
| Whole internet stops on laptop | AllowedIPs = 0.0.0.0/0 full tunnel + DNS mismatch | Split tunnel, or add exclusion routes for local nets |
Next Steps / Ops Actions
- Restrict which VPN subnets reach which zones: network-segmentation.
- Alert on stale handshakes via Prometheus/snmp checks: prometheus-basics.
- Keep the hub host hardened: ssh-security-hardening, fail2ban-setup.
Sources & Related
External references consulted:
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB build session.