Infrastructure

  • WireGuard
  • VPN
  • Linux
  • Security
  • Infrastructure

WireGuard VPN: A Production Setup Guide for Remote Teams (2026)

Production WireGuard setup: wg0.conf, split vs full tunnel, DNS, MTU, peer lifecycle, monitoring, and firewall patterns for remote teams. Step-by-step guide.

· Updated Jun 7, 2026· 17 min read· Varnox Team

Remote access is easy to demo and hard to operate: keys on sticky notes, “temporary” full tunnels, iptables nobody remembers, DNS that still leaks to the ISP. WireGuard does not fix organizational discipline — but it is simpler than OpenVPN or IPsec, which makes good ops practices easier to maintain.

WireGuard is an open protocol and kernel module (mainline since Linux 5.6): you choose the server, generate keys, write forwarding rules, and own the runbook. The gateway should sit on a hardened Linux host — SSH, firewall, patching, and backups before you expose UDP 51820. Our Linux server hardening checklist is the baseline we use before any VPN cutover.

This guide covers production-shaped wg0.conf, client setup on common platforms, split vs full tunneling, DNS, MTU, peer lifecycle, monitoring, OpenVPN migration, and firewall patterns.

What WireGuard Actually Is: The Cryptography

WireGuard's entire codebase is roughly 4,000 lines — orders of magnitude smaller than OpenVPN or IPsec. Small codebases have smaller attack surfaces. Here is exactly what it uses:

Key exchange: Curve25519 (an elliptic curve Diffie-Hellman function). Each peer has a 32-byte private key and a corresponding 32-byte public key. The shared secret is derived from ECDH.

Symmetric encryption: ChaCha20-Poly1305. ChaCha20 is the stream cipher; Poly1305 is the MAC. This is the same AEAD construction used in TLS 1.3. It is hardware-accelerated on ARM (which matters on embedded servers, Raspberry Pi, mobile devices).

Hashing: BLAKE2s — a fast, secure hash used for key derivation and handshake state.

Handshake construction: Noise protocol framework (IKpsk2 pattern). The handshake provides forward secrecy via ephemeral keys generated per-session.

Preshared keys (PSK): Optional symmetric keys layered on the handshake. WireGuard documentation describes these as an additional layer of symmetric encryption; some teams use them as defense-in-depth. They are not a substitute for a post-quantum VPN protocol.

Rekeying: Session keys rotate automatically based on time and traffic volume (WireGuard’s defaults rekey after roughly two minutes or a set message count). You do not manage this manually.

WireGuard operates at Layer 3. It creates a virtual network interface (wg0 by default) and routes IP packets through the tunnel. There is no TCP inside the VPN — WireGuard uses UDP exclusively, which reduces head-of-line blocking and works better across NAT.

Server Prerequisites

You need a Linux server (Ubuntu 22.04 LTS or Debian 12 recommended) with:

  • A public IP address (or a domain pointing to one)
  • UDP port 51820 open (or any port you choose)
  • Root or sudo access
  • Kernel 5.6+ (Ubuntu 22.04 ships 5.15; Debian 12 ships 6.1 — both fine)

If your kernel is older (Ubuntu 20.04 ships 5.4), WireGuard is available directly from the official Ubuntu repos — the PPA is archived and no longer needed:

# Ubuntu 20.04 — available via official backports, no PPA required
apt update && apt install -y wireguard wireguard-tools

For Ubuntu 22.04+, Debian 11+:

apt update && apt install -y wireguard wireguard-tools

WireGuard tools installs wg and wg-quick. The kernel module loads automatically.

Verify the module is loaded:

lsmod | grep wireguard
# wireguard             86016  0
# curve25519_x86_64     36864  1 wireguard
# libcurve25519_generic 49152  2 wireguard,curve25519_x86_64

Key Generation

WireGuard uses static public/private key pairs. Generate one pair for the server and one for each peer (client).

# Generate server keys
cd /etc/wireguard
umask 077  # Critical: restrict file permissions before creating keys
wg genkey | tee server_private.key | wg pubkey > server_public.key

# Verify permissions
ls -la /etc/wireguard/
# -rw------- 1 root root  45 Jan 10 09:00 server_private.key
# -rw------- 1 root root  45 Jan 10 09:00 server_public.key

wg genkey generates a Curve25519 private key. The pipe to wg pubkey derives the corresponding public key. Both are base64-encoded 32-byte values.

Generate a preshared key (PSK) per peer for defense-in-depth:

wg genpsk > peer1_psk.key

Store private keys and PSKs server-side. Public keys are safe to share openly.

Server Configuration: wg0.conf

The main config file lives at /etc/wireguard/wg0.conf. Here is a production-ready template with annotations:

[Interface]
# The server's private key — NEVER share this
PrivateKey = <server_private_key_here>

# The VPN subnet address for this server. 10.10.0.1 is the server's VPN IP.
# /24 gives you 254 usable peer addresses (10.10.0.2 through 10.10.0.254)
Address = 10.10.0.1/24

# UDP port WireGuard listens on. Default is 51820. Change to 443 or 80
# if you're behind a restrictive firewall that only allows common ports.
ListenPort = 51820

# DNS server for peers (when using full tunnel). Use your internal resolver
# or a public one like Cloudflare (1.1.1.1) or Quad9 (9.9.9.9)
# This is only pushed to peers that set DNS in their [Interface] block.

# PostUp/PostDown run as root when the interface comes up or down.
# These rules NAT traffic from the VPN subnet to the internet-facing interface.
# Replace eth0 with your actual outbound interface (check: ip route | grep default)
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

# IPv6 NAT — add if you need IPv6 support
# PostUp = ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# PostDown = ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Peer 1: Laptop (Alice)
[Peer]
# Alice's public key — generated on her device
PublicKey = <alice_public_key>
# PSK — optional extra symmetric layer
PresharedKey = <alice_psk>
# The VPN IP assigned to Alice. AllowedIPs acts as a routing table:
# traffic to 10.10.0.2 goes through this peer's tunnel
AllowedIPs = 10.10.0.2/32

# Peer 2: Bob's workstation
[Peer]
PublicKey = <bob_public_key>
PresharedKey = <bob_psk>
AllowedIPs = 10.10.0.3/32

# Peer 3: A subnet (site-to-site scenario)
# If Bob's peer represents an entire office network (192.168.100.0/24),
# add that subnet here so traffic to it routes through Bob's tunnel
[Peer]
PublicKey = <office_router_public_key>
AllowedIPs = 10.10.0.10/32, 192.168.100.0/24

The %i in PostUp/PostDown expands to the interface name (wg0).

Enable IP Forwarding

Without IP forwarding enabled, the server will not route VPN traffic to the internet. This is the most common mistake in WireGuard setups.

# Check current state
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0  ← disabled, traffic won't route

# Enable permanently
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf
echo "net.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf

# Verify
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1

Starting WireGuard and Enabling at Boot

wg-quick is the standard helper that reads wg0.conf, creates the interface, sets routes, runs PostUp, and starts listening.

# Start the interface
wg-quick up wg0

# Verify it's running
wg show
# interface: wg0
#   public key: <server_public_key>
#   private key: (hidden)
#   listening port: 51820
#
# peer: <alice_public_key>
#   preshared key: (hidden)
#   allowed ips: 10.10.0.2/32
#   latest handshake: 2 minutes, 14 seconds ago
#   transfer: 1.42 MiB received, 3.18 MiB sent

# Enable at boot via systemd
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0

Client Configuration

Generate keys on the client side (the same wg genkey / wg pubkey commands). Add the client's public key to the server's wg0.conf as a new [Peer] block, then reload:

# On server: reload config without dropping existing connections
wg syncconf wg0 <(wg-quick strip wg0)
# or for a full restart:
systemctl restart wg-quick@wg0

Linux Client (/etc/wireguard/wg0.conf)

[Interface]
PrivateKey = <client_private_key>
Address = 10.10.0.2/24
# DNS server — use the server's VPN IP if you run a resolver there,
# or 1.1.1.1 for full-tunnel internet access
DNS = 1.1.1.1

[Peer]
# Server's public key
PublicKey = <server_public_key>
PresharedKey = <psk_for_this_peer>
# For full tunnel (all traffic through VPN): 0.0.0.0/0, ::/0
# For split tunnel (only VPN subnet): 10.10.0.0/24
AllowedIPs = 0.0.0.0/0, ::/0
# Server's public IP/hostname and port
Endpoint = vpn.yourdomain.com:51820
# Keeps the NAT hole open — sends keepalive every 25 seconds.
# Required when the client is behind NAT (home/office router).
PersistentKeepalive = 25

macOS Client

Install WireGuard from the Mac App Store. The config format is identical to Linux. Import via File > Import Tunnel(s) from File or paste into the app.

Alternatively via Homebrew:

brew install wireguard-tools
# Config lives at /usr/local/etc/wireguard/wg0.conf
wg-quick up wg0

Windows Client

Download the official client from wireguard.com. Import the .conf file. The same config format applies.

iOS / Android

Use the official WireGuard app. Import via QR code — generate one from the config file:

# On server, after creating the client config
apt install qrencode
qrencode -t ansiutf8 < /etc/wireguard/peer_alice_mobile.conf

Scan the QR code in the WireGuard app. Never share QR codes over unencrypted channels.

Split Tunneling vs Full Tunnel

This is controlled entirely by AllowedIPs in the client's peer block.

Full tunnel — all traffic (including internet browsing) routes through the server:

AllowedIPs = 0.0.0.0/0, ::/0

Traffic flow: client → VPN server → internet. The client's public IP becomes the server's IP.

Split tunnel — only traffic to the VPN subnet goes through the tunnel; internet traffic goes directly:

AllowedIPs = 10.10.0.0/24

Traffic flow: client → VPN server for 10.10.0.0/24 only. Internet traffic bypasses the VPN.

Split tunnel with specific subnets — add specific office or cloud subnets:

AllowedIPs = 10.10.0.0/24, 192.168.50.0/24, 172.16.0.0/12

The subnet exclusion trick: WireGuard has no native "exclude this subnet" option. To route everything except a specific network, use CIDR math. For example, to route all traffic except 192.168.1.0/24, you need the complement. Tools like wg-routes.com/calculator compute the required CIDR list.

DNS Leak Prevention

When using a full tunnel with AllowedIPs = 0.0.0.0/0, DNS queries must also go through the tunnel. If they don't, your ISP sees your DNS requests — a "DNS leak."

Set DNS in the client's [Interface]:

[Interface]
DNS = 10.10.0.1  # Use the server's VPN IP if it runs a resolver (unbound, pihole, etc.)
# or a public resolver you trust
DNS = 1.1.1.1, 9.9.9.9

wg-quick on Linux calls resolvconf or writes to /etc/resolv.conf to set this DNS. On Windows and macOS, the WireGuard app handles it natively.

Test for DNS leaks: With the VPN active, visit dnsleaktest.com or run:

dig +short whoami.akamai.net @ns1-1.akamaitech.net
# Should return the VPN server's IP, not your local ISP's IP

Run a DNS resolver on the VPN server for internal name resolution (Unbound is lightweight):

apt install unbound
# /etc/unbound/unbound.conf
# interface: 10.10.0.1
# access-control: 10.10.0.0/24 allow
# forward-zone:
#   name: "."
#   forward-addr: 1.1.1.1

MTU Tuning

WireGuard's default MTU is 1420 bytes. The overhead is:

  • IPv4 header: 20 bytes
  • UDP header: 8 bytes
  • WireGuard header: 32 bytes
  • Total overhead: 60 bytes

Standard Ethernet MTU is 1500 bytes, so 1500 − 60 = 1440, but WireGuard uses 1420 as a conservative default to account for PPPoE links (which have their own 8-byte overhead: 1500 − 60 − 8 = 1432, rounded down).

If you run WireGuard over a network with a smaller MTU (common with some VPS providers, cable modems):

# Check path MTU to your server
tracepath vpn.yourdomain.com | grep pmtu
# pmtu 1492 (PPPoE) or 1452 (some DSL)

Set MTU in the server's [Interface]:

[Interface]
MTU = 1380  # Conservative value for PPPoE; test with ping -M do -s 1352 vpn.yourdomain.com

Symptoms of MTU problems: large file transfers stall while small packets work fine, SSH sessions connect but interactive sessions freeze.

Multi-Peer Management

For teams of 10+ users, managing individual [Peer] blocks in wg0.conf gets unwieldy. Options:

Using wg set for live changes (no restart)

# Add a peer without restarting the interface
wg set wg0 peer <new_public_key> preshared-key <psk_file> allowed-ips 10.10.0.5/32

# Remove a peer
wg set wg0 peer <public_key> remove

# Save the running config back to wg0.conf
wg showconf wg0 > /etc/wireguard/wg0.conf

wg syncconf for config file changes

# Edit wg0.conf to add/remove peers, then sync without dropping sessions
wg syncconf wg0 <(wg-quick strip wg0)

wg-quick strip removes wg-quick-specific directives (Address, DNS, PostUp, etc.) before passing to wg syncconf, which only understands native WireGuard config syntax.

Inventory file approach

Keep a separate file tracking peer assignments:

# /etc/wireguard/peers.txt
# name          vpn_ip          public_key                              psk_file
alice-laptop    10.10.0.2/32    <key>                                   alice.psk
alice-mobile    10.10.0.3/32    <key>                                   alice_mobile.psk
bob-workstation 10.10.0.4/32    <key>                                   bob.psk

Write a small shell script to regenerate wg0.conf from this inventory. This is the manual equivalent of tools like wg-easy or WireHole.

Key Rotation: Zero-Downtime Procedure

Keys should be rotated periodically (annually is a common policy) or immediately when a device is lost. WireGuard sessions survive a key rotation if you sequence it correctly.

Step 1: Generate new keys on the client device:

umask 077
wg genkey | tee new_private.key | wg pubkey > new_public.key
wg genpsk > new_psk.key

Step 2: Add the new peer entry to the server before removing the old one:

# Add new key alongside old key
wg set wg0 peer <new_public_key> preshared-key /etc/wireguard/alice_new.psk allowed-ips 10.10.0.2/32

At this point both old and new keys are active.

Step 3: Update the client config to use the new private key and PSK. Reconnect the client.

Step 4: Verify the client is connected with the new key:

wg show wg0
# Confirm new key shows a recent handshake timestamp

Step 5: Remove the old key:

wg set wg0 peer <old_public_key> remove
wg showconf wg0 > /etc/wireguard/wg0.conf

Monitoring: Are Peers Actually Connected?

wg show gives you the last handshake timestamp per peer. If a peer's handshake is more than 3 minutes old, the connection is effectively idle (though it will reconnect on next traffic).

wg show wg0 latest-handshakes
# <public_key>   1746432819
# Convert to human time:
date -d @1746432819

Write a monitoring script that alerts when a peer hasn't handshaked recently:

#!/bin/bash
# /usr/local/bin/wg-monitor.sh
THRESHOLD=300  # 5 minutes in seconds
NOW=$(date +%s)

wg show all latest-handshakes | while read iface pubkey timestamp; do
    if [ "$timestamp" = "0" ]; then
        echo "WARN: $pubkey on $iface has never connected"
        continue
    fi
    AGE=$(( NOW - timestamp ))
    if [ $AGE -gt $THRESHOLD ]; then
        echo "WARN: $pubkey on $iface last seen ${AGE}s ago"
    fi
done

For Prometheus monitoring, wireguard_exporter exposes peer metrics including handshake age, bytes sent/received, and endpoint address.

Check connection status from a peer's perspective:

# On the client
ping 10.10.0.1  # Ping the server's VPN IP
# Round-trip time should be low (≤10ms for nearby servers)

Firewall Configuration

UFW (Uncomplicated Firewall)

# Allow WireGuard UDP port
ufw allow 51820/udp comment 'WireGuard'

# Allow forwarding in UFW — edit /etc/default/ufw
DEFAULT_FORWARD_POLICY="ACCEPT"

# Or add to /etc/ufw/before.rules, BEFORE the *filter section:
# *nat
# :POSTROUTING ACCEPT [0:0]
# -A POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
# COMMIT

ufw reload

nftables (modern alternative to iptables)

# /etc/nftables.conf
table inet wireguard {
    chain forward {
        type filter hook forward priority 0; policy drop;
        iifname "wg0" accept
        oifname "wg0" ct state established,related accept
    }
    chain postrouting {
        type nat hook postrouting priority srcnat;
        iifname "wg0" oifname "eth0" masquerade
    }
}

Apply:

nft -f /etc/nftables.conf
systemctl enable nftables

Migrating from OpenVPN

The typical migration runs OpenVPN and WireGuard in parallel for 2–4 weeks.

Phase 1: Deploy WireGuard alongside OpenVPN

Keep OpenVPN on its port (1194/udp or 443/tcp). Deploy WireGuard on 51820/udp. Both are active.

Phase 2: Migrate pilot users

Give 2–3 technically comfortable users WireGuard configs. Collect feedback on connectivity and performance.

Phase 3: Roll out WireGuard to all users

Generate configs for all users. Distribute securely (not email — use a password manager, secure file share, or QR code in person).

Phase 4: Monitor

Run both for 2 weeks. Watch OpenVPN connection logs — when connections drop to zero, decommission it.

Phase 5: Remove OpenVPN

systemctl stop openvpn@server
systemctl disable openvpn@server
ufw delete allow 1194/udp

Key difference to communicate to users: WireGuard client configs are static key files, not username/password. Losing the device means revoking the key — not resetting a password. Factor this into your onboarding documentation.

Common Troubleshooting

Handshake never completes:

  • Check the server's UDP port is reachable: nc -zvu vpn.yourdomain.com 51820
  • Verify firewall allows 51820/udp on the server
  • Confirm the client has the correct server public key and endpoint IP
  • Check dmesg | grep wireguard on the server for crypto errors

Connected but can't reach the internet:

  • IP forwarding: sysctl net.ipv4.ip_forward must be 1
  • NAT rules: iptables -t nat -L POSTROUTING -n -v — look for the MASQUERADE rule
  • Check you're using the right outbound interface in PostUp (eth0 vs ens3 vs ens18)

Connected but can't reach the internet on only some sites:

  • MTU issue. Test: ping -M do -s 1400 8.8.8.8 (from behind the VPN). Reduce MTU if it fails.

DNS not resolving:

  • cat /etc/resolv.conf on the client — should show the DNS set in [Interface]
  • If using systemd-resolved: resolvectl status to verify DNS settings per interface

Peer can't reach other peers (site-to-site):

  • Add the remote subnet to AllowedIPs on both ends
  • Ensure IP forwarding is enabled on both ends
  • Check routing tables: ip route show and ip route show table all

Security Hardening Checklist

  • Keys generated with umask 077 — permissions 600 on all key files
  • Private keys stored only where needed (server side: server private key; client side: client private key)
  • Preshared keys (PSK) configured per peer if your policy requires them
  • PermitRootLogin no in /etc/ssh/sshd_config (SSH is separate but you're already on that server)
  • AllowedIPs set to minimum necessary (avoid 0.0.0.0/0 on the server unless it's a road-warrior concentrator)
  • WireGuard interface not externally reachable on management ports
  • Offboarding procedure documented and tested: lost device → key removal in under 5 minutes
  • Key rotation policy documented (annual minimum; immediately on device loss)
  • Audit log: who has which public key, when added, which device
  • PersistentKeepalive set only on clients behind NAT (not on servers)

Production Architecture Patterns

Road-warrior concentrator (most common): One cloud server, all employees connect to it. Full tunnel so the server's IP is the egress IP for all VPN traffic.

Site-to-site: Two or more servers (office A, office B, cloud). Each server is a peer of the others. AllowedIPs includes the remote subnet.

Hub-and-spoke with split access: Server has multiple peers; AllowedIPs per peer restricts what each peer can reach. Engineering can reach the database subnet; contractors cannot.

Mesh network: Every peer can communicate directly. Each device has entries for every other device. Scales poorly past ~20 peers but works well for small engineering teams.

For teams beyond 30 users, consider a management layer: Headscale (self-hosted Tailscale control plane) or Netbird add device inventory, key distribution, and ACLs on top of WireGuard without replacing the protocol.


We roll out WireGuard for distributed teams as part of our VPN and infrastructure work: hardened Linux host, peer configs per device, NAT and DNS matched to your threat model, and written runbooks for revoke, onboard, and key rotation.

Send a brief if you want help scoping this for your team.


Related articles

← Back to blog

Book your free consultation