Installs into .claude/skills of the current project.
Are you the author of Wireguard?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-wireguard)
---
name: wireguard
description: |
WireGuard VPN setup on Linux. Covers server config, client config, key management,
NAT/IP forwarding, peer management, split tunnel, full tunnel, and site-to-site.
USE WHEN: user mentions "wireguard", "wg0", "wg-quick", "wireguard vpn",
"wireguard server setup", "wireguard peer", "wireguard keys", "wireguard client",
"wireguard nat", "wireguard iptables", "wireguard split tunnel", "wireguard qr code",
"wg genkey", "wireguard site-to-site", "wireguard keepalive"
DO NOT USE FOR: OpenVPN setups - different tool and protocol,
IPsec / IKEv2 VPNs,
Tailscale or Netbird (they use WireGuard underneath but have their own CLIs),
Cloud VPN gateways (AWS VPN, GCP Cloud VPN)
allowed-tools: Read, Grep, Glob, Write, Edit, Bash
---
# WireGuard Core Knowledge
## Concepts
| Term | Meaning |
|---|---|
| Interface | Virtual network adapter (`wg0`). One per VPN tunnel. |
| Private key | Generated per peer; never shared. |
| Public key | Derived from private key; exchanged with remote peers. |
| Peer | Any other WireGuard node (server or client). |
| AllowedIPs | IP ranges allowed through the tunnel to/from this peer. Acts as routing table + firewall. |
| Endpoint | `IP:port` where this peer is reachable (optional on server for dynamic clients). |
| PersistentKeepalive | Sends keepalive every N seconds — required for clients behind NAT. |
| PreSharedKey | Optional symmetric key for additional post-quantum protection. |
---
## Server Setup
### Install WireGuard
```bash
# Ubuntu / Debian
sudo apt update && sudo apt install -y wireguard wireguard-tools
# RHEL / Rocky / Alma
sudo dnf install -y epel-release && sudo dnf install -y wireguard-tools
# Arch
sudo pacman -S wireguard-tools
# Verify kernel module loaded (Linux 5.6+: built in)
sudo modprobe wireguard && echo "WireGuard module OK"
```
### Generate Server Keys
```bash
# Create key directory with tight permissions
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
# Generate server key pair
wg genkey | sudo tee /etc/wireguard/server_private.key | \
wg pubkey | sudo tee /etc/wireguard/server_public.key
sudo chmod 600 /etc/wireguard/server_private.key
cat /etc/wireguard/server_public.key # Share this with clients
```
### Server Config — `/etc/wireguard/wg0.conf`
```ini
[Interface]
# Server private key (keep secret)
PrivateKey = <SERVER_PRIVATE_KEY>
# VPN subnet address for the server itself
Address = 10.10.0.1/24
# UDP port WireGuard listens on
ListenPort = 51820
# IP forwarding rules — applied when wg-quick brings up the interface
# Replace eth0 with your actual outbound interface (ip route | grep default)
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostUp = iptables -A FORWARD -o %i -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# IPv6 forwarding (optional)
PostUp = sysctl -w net.ipv6.conf.all.forwarding=1
PostUp = ip6tables -A FORWARD -i %i -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -D FORWARD -i %i -j ACCEPT
PreDown = iptables -D FORWARD -o %i -j ACCEPT
PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PreDown = ip6tables -D FORWARD -i %i -j ACCEPT
PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# Save/restore rules between restarts
SaveConfig = false # Set true only during testing; keeps config clean in prod
# --- Peers (one block per client) ---
[Peer]
# Client: Alice's laptop
PublicKey = <ALICE_PUBLIC_KEY>
# Unique IP assigned to Alice's device in the VPN subnet
AllowedIPs = 10.10.0.2/32
# No Endpoint here — Alice connects from dynamic IPs
# PresharedKey = <OPTIONAL_PSK> # Uncomment for additional security
[Peer]
# Client: Bob's phone
PublicKey = <BOB_PUBLIC_KEY>
AllowedIPs = 10.10.0.3/32
[Peer]
# Site-to-site: Office network (192.168.50.0/24)
PublicKey = <OFFICE_GW_PUBLIC_KEY>
AllowedIPs = 10.10.0.10/32, 192.168.50.0/24
Endpoint = office-gw.example.com:51820
PersistentKeepalive = 25
```
### Enable IP Forwarding Permanently
```bash
# /etc/sysctl.conf or /etc/sysctl.d/99-wireguard.conf
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
```
### Start and Enable via systemd
```bash
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0
# Check interface
sudo wg show wg0
```
---
## Client Config Generation
### Generate Client Keys
```bash
# Run on the client machine (or generate server-side and distribute securely)
wg genkey | tee client_private.key | wg pubkey > client_public.key
chmod 600 client_private.key
# Optional preshared key (same for both sides)
wg genpsk > client_psk.key
```
### Client Config — Full Tunnel (all traffic through VPN)
```ini
# /etc/wireguard/wg0.conf (on client)
[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.10.0.2/32 # Unique VPN IP assigned to this client
DNS = 10.10.0.1 # Server acts as DNS resolver; or use 1.1.1.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = vpn.example.com:51820
# Full tunnel: all traffic (including internet) through VPN
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25 # Required when client is behind NAT
# PresharedKey = <PSK>
```
### Client Config — Split Tunnel (only internal subnets through VPN)
```ini
[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.10.0.2/32
DNS = 10.10.0.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = vpn.example.com:51820
# Split tunnel: only route private subnets through VPN
AllowedIPs = 10.10.0.0/24, 192.168.1.0/24, 172.16.0.0/12
# Internet traffic goes directly (NOT through VPN)
PersistentKeepalive = 25
```
---
## QR Code for Mobile Clients
```bash
# Install qrencode
sudo apt install -y qrencode
# Print QR code to terminal (scan with WireGuard iOS/Android app)
qrencode -t ansiutf8 < /etc/wireguard/clients/alice.conf
# Or export to PNG
qrencode -o alice-vpn.png < /etc/wireguard/clients/alice.conf
```
---
## Peer Management (Hot Reload — No Restart Required)
### Add a Peer Without Restart
```bash
# Generate new peer keys
wg genkey | tee /etc/wireguard/clients/carol_private.key | \
wg pubkey > /etc/wireguard/clients/carol_public.key
CAROL_PUBKEY=$(cat /etc/wireguard/clients/carol_public.key)
# Add peer to running interface
sudo wg set wg0 peer "$CAROL_PUBKEY" allowed-ips 10.10.0.4/32
# Persist to config file
sudo wg-quick save wg0 # ONLY works if SaveConfig=true
# OR append to conf manually:
echo -e "\n[Peer]\nPublicKey = $CAROL_PUBKEY\nAllowedIPs = 10.10.0.4/32" \
| sudo tee -a /etc/wireguard/wg0.conf
```
### Remove a Peer Without Restart
```bash
PEER_PUBKEY="<PUBLIC_KEY_TO_REMOVE>"
sudo wg set wg0 peer "$PEER_PUBKEY" remove
# Remove the [Peer] block from wg0.conf manually to persist
```
### Check Peer Status
```bash
# Show all peers, latest handshake, transfer stats
sudo wg show wg0
# Show only handshake times (0 = never connected)
sudo wg show wg0 latest-handshakes
# Show allowed IPs
sudo wg show wg0 allowed-ips
# Check transfer per peer
sudo wg show wg0 transfer
```
---
## Site-to-Site Pattern
```
Office LAN (192.168.50.0/24) Cloud LAN (10.10.0.0/24)
| |
[Office GW] ──── WireGuard tunnel ──── [Cloud GW / Server]
wg0: 10.10.0.10 wg0: 10.10.0.1
```
**Cloud Server peer for office:**
```ini
[Peer]
PublicKey = <OFFICE_GW_PUBLIC_KEY>
AllowedIPs = 10.10.0.10/32, 192.168.50.0/24
Endpoint = office-public-ip:51820
PersistentKeepalive = 25
```
**Office Gateway config:**
```ini
[Interface]
PrivateKey = <OFFICE_GW_PRIVATE_KEY>
Address = 10.10.0.10/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <CLOUD_SERVER_PUBLIC_KEY>
AllowedIPs = 10.10.0.0/24 # Cloud VPN subnet only (split tunnel)
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25
```
---
## Monitoring and Transfer Stats
```bash
# Live monitoring
watch -n 2 sudo wg show
# Handshake age (should be < 3 min for active clients with keepalive=25)
sudo wg show wg0 latest-handshakes | awk '{
now = systime(); age = now - $2;
printf "%s last handshake: %d seconds ago\n", $1, age
}'
# Total transferred (bytes) — parse for alerting
sudo wg show wg0 transfer
# Interface stats via ip
ip -s link show wg0
```
---
## DNS for VPN Clients
### Option 1: systemd-resolved on server as DNS resolver
```bash
# Allow DNS queries from VPN subnet
sudo apt install -y systemd-resolved
# Edit /etc/systemd/resolved.conf
[Resolve]
DNS=1.1.1.1 8.8.8.8
DNSStubListener=yes
# Allow port 53 from VPN interface
sudo iptables -A INPUT -i wg0 -p udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -i wg0 -p tcp --dport 53 -j ACCEPT
```
Clients set `DNS = 10.10.0.1` in their `[Interface]` block.
### Option 2: dnsmasq for internal hostname resolution
```bash
sudo apt install -y dnsmasq
# /etc/dnsmasq.conf
interface=wg0
bind-interfaces
domain=vpn.internal
expand-hosts
local=/vpn.internal/
# Forward everything else to upstream
server=1.1.1.1
server=8.8.8.8
```
---
## Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Sharing private keys between peers | If one device is compromised, all peers with that key are compromised | Generate a unique key pair per device; never copy private keys |
| Not setting `PersistentKeepalive` for NAT clients | NAT mapping expires → client appears disconnected after idle period | Set `PersistentKeepalive = 25` on all clients behind NAT/firewall |
| Forgetting `net.ipv4.ip_forward = 1` | Traffic enters VPN but is not forwarded → clients can reach server but nothing else | Set via sysctl.d file and apply `PostUp` in wg0.conf |
| Using `SaveConfig = true` in production | `wg-quick save` overwrites conf on shutdown, blowing away comments and formatting | Use `SaveConfig = false`; manage conf file manually or via config management |
| Assigning overlapping AllowedIPs to two peers | WireGuard silently uses the last-defined peer for a given IP range | Assign unique, non-overlapping /32 addresses per client |
| Opening port 51820 to `0.0.0.0/0` in all directions | Exposure is unavoidable for UDP listen port, but ensure firewall blocks other ports | Firewall all ports except 22 (SSH), 51820 (WireGuard), and your services |
| Full tunnel (`0.0.0.0/0`) without DNS set | DNS leaks — queries go to ISP DNS outside the tunnel | Always set `DNS` in client `[Interface]` when using full tunnel |
| Distributing client configs over unencrypted channels | Private key exposed in transit | Transfer via encrypted channel (SFTP, gpg-encrypted file, Signal, QR code over physical display) |
| Leaving unused peers in config | Revoked clients remain technically valid | Remove both the `[Peer]` block from wg0.conf and run `wg set wg0 peer <key> remove` |
| Not logging handshake timestamps | No visibility into active vs stale clients | Set up periodic cron to log `wg show wg0 latest-handshakes` to a monitoring system |
---
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| No handshake ever established | Endpoint unreachable or firewall blocking port 51820/UDP | `nc -u vpn.example.com 51820` from client; check server firewall: `sudo ufw allow 51820/udp` |
| Handshake established but traffic not routing | `ip_forward` not enabled, iptables FORWARD chain dropping packets | `sysctl net.ipv4.ip_forward` must return `1`; check `iptables -L FORWARD` |
| DNS not working inside VPN | Client DNS set to VPN server IP but DNS not running on server | Install dnsmasq/systemd-resolved; allow port 53 from wg0 interface |
| Client connects but internet not reachable (full tunnel) | NAT/MASQUERADE rule missing | Verify `iptables -t nat -L POSTROUTING` shows MASQUERADE on correct interface |
| Connection drops every few minutes | No keepalive and NAT mapping expires | Add `PersistentKeepalive = 25` to client config |
| "RTNETLINK answers: Operation not supported" on modprobe | Kernel too old (< 5.6) or WireGuard not installed | Install `wireguard-dkms` package or upgrade kernel |
| Peers listed in `wg show` but can't ping VPN server | AllowedIPs mismatch — client AllowedIPs doesn't include server VPN IP | Ensure server VPN IP (10.10.0.1) is within client's AllowedIPs range |
| `wg-quick: line X: invalid option` | wg0.conf syntax error | Run `wg-quick strip wg0` to validate conf; check for tabs vs spaces |
| IP address conflict with LAN | Client VPN subnet overlaps with local network | Change VPN subnet to unused range (e.g., 10.88.0.0/24 instead of 10.0.0.0/24) |
| Can't SSH to server after bringing up wg0 | iptables FORWARD chain blocking return traffic | Add `iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT` |
---
## Production Checklist
- [ ] Unique key pair per device (never share private keys)
- [ ] `/etc/wireguard/` permissions: directory `700`, private key files `600`
- [ ] `net.ipv4.ip_forward = 1` in `/etc/sysctl.d/`
- [ ] iptables MASQUERADE rule set in `PostUp` / cleaned in `PreDown`
- [ ] `PersistentKeepalive = 25` on all NAT clients
- [ ] `SaveConfig = false` in production (manage config via code)
- [ ] `systemctl enable wg-quick@wg0` for auto-start on reboot
- [ ] Firewall: only port 51820/UDP open externally
- [ ] Client configs distributed via encrypted channel
- [ ] Unused peers removed promptly from config and live interface
- [ ] DNS set for full-tunnel clients to prevent DNS leaks
- [ ] Monitoring: cron/alert on peers with no handshake in > 5 minutes