WireGuard is the simplest full-featured VPN protocol available today. It's about 4,000 lines of code (versus hundreds of thousands for OpenVPN or IPsec), runs in the Linux kernel, uses modern cryptography with no cipher negotiation, and achieves throughput that trades favorably against everything else at its simplicity level.
This post covers the practical setup for a site-to-site VPN โ connecting two or more networks so that hosts on each can reach hosts on the others. It's not a client-VPN setup (where individual users connect to a central gateway), though many of the concepts are the same.
Concepts before config
WireGuard works with peers, not clients and servers. Each WireGuard interface has a private key and a list of peers, each identified by their public key. A peer can initiate connections to you, you can initiate to them, or both. This peer model is what makes site-to-site configuration natural: both sides are equal participants.
Key concepts:
- Interface: A virtual network interface (
wg0) with its own IP address on the VPN subnet - Peer: Another WireGuard endpoint you want to connect to, identified by public key
- AllowedIPs: The IP ranges that traffic to/from this peer should be routed through the tunnel. This is also used for cryptographic routing โ WireGuard decrypts packets only from the peer whose public key corresponds to the source IP
- Endpoint: The real IP address and UDP port of a peer (optional โ if omitted, WireGuard learns it from incoming packets)
Site-to-site: two offices, one tunnel
Scenario: two sites, each behind NAT, each with a Linux gateway. Site A has LAN subnet 192.168.1.0/24, Site B has 192.168.2.0/24. We want hosts on each site to reach hosts on the other.
Step 1: Generate keys on each gateway
# On each gateway (Site A and Site B)
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
chmod 600 /etc/wireguard/privatekey
# Show keys
cat /etc/wireguard/privatekey # keep secret
cat /etc/wireguard/publickey # share with the other side
Step 2: Configure Site A gateway
# /etc/wireguard/wg0.conf on Site A gateway
[Interface]
PrivateKey = <site-a-private-key>
Address = 10.100.0.1/24 # VPN tunnel IP for this gateway
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <site-b-public-key>
Endpoint = site-b-public-ip:51820 # Site B's public IP and port
AllowedIPs = 10.100.0.2/32, 192.168.2.0/24 # VPN IP + Site B's LAN
PersistentKeepalive = 25 # keep tunnel alive through NAT
Step 3: Configure Site B gateway (mirror)
# /etc/wireguard/wg0.conf on Site B gateway
[Interface]
PrivateKey = <site-b-private-key>
Address = 10.100.0.2/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <site-a-public-key>
Endpoint = site-a-public-ip:51820
AllowedIPs = 10.100.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25
Step 4: Enable IP forwarding and bring up the interface
# On both gateways
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p
# Enable and start WireGuard
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
# Verify
wg show # shows peers, latest handshake, transfer stats
wg showconf wg0 # shows current running config
Step 5: Route LAN traffic through the gateway
Hosts on each LAN need to know that the other site's subnet is reachable through the WireGuard gateway. Either add static routes on each host, or configure your router to advertise the route:
# Static route on a host in Site A to reach Site B's LAN
ip route add 192.168.2.0/24 via 192.168.1.1 # where .1 is Site A's gateway
# Or in /etc/network/interfaces (persistent):
up ip route add 192.168.2.0/24 via 192.168.1.1
Testing the tunnel: Run wg show and look for latest handshake under each peer โ if it shows a time within the last 2 minutes, the tunnel is up. Then ping 10.100.0.2 (the other gateway's VPN IP), then ping 192.168.2.1 (a host in the remote LAN). Work through each hop systematically.
Key rotation
WireGuard's Noise protocol provides forward secrecy at the session level โ session keys rotate automatically every few minutes. But the long-term identity keys (the ones in your config files) don't rotate automatically. For infrastructure you trust, annual rotation is a reasonable cadence. The process:
# 1. Generate new keypair on the target gateway
wg genkey | tee /etc/wireguard/privatekey.new | wg pubkey > /etc/wireguard/publickey.new
# 2. Update the [Interface] section with the new private key
# 3. Send the new public key to all peers โ they update their [Peer] sections
# 4. Restart WireGuard: systemctl restart wg-quick@wg0
# 5. Verify tunnel comes back up before cleaning up old keys
For automated key rotation at scale, look at wg-dynamic (WireGuard's own dynamic key exchange protocol, still experimental) or manage keys through a secrets store like HashiCorp Vault with WireGuard key material stored as KV secrets and distributed via your configuration management.
DNS split-horizon for multi-site
Once the tunnel is up, services on each site are reachable by IP. But you probably want hostnames to resolve differently depending on which site you're on โ db.internal should resolve to the local database, not the remote one. This is DNS split-horizon.
Run a local DNS resolver on each site (Unbound, CoreDNS, or dnsmasq) configured to:
- Resolve internal hostnames locally (e.g.
*.site-a.internalโ Site A's authoritative DNS) - Forward cross-site hostnames to the other site's resolver over the WireGuard tunnel
- Forward all other queries upstream
# CoreDNS Corefile example (Site A)
.:53 {
forward . 8.8.8.8 8.8.4.4
cache
log
}
site-a.internal:53 {
file /etc/coredns/zones/site-a.internal
log
}
site-b.internal:53 {
# Forward to Site B's DNS resolver over the WireGuard tunnel
forward . 192.168.2.53
log
}
WireGuard in a zero-trust architecture
WireGuard connects networks. Zero-trust says you shouldn't trust the network. These aren't contradictory โ they operate at different layers. WireGuard provides encrypted transport between sites, but it doesn't replace identity-aware access control for individual services. In a zero-trust setup, you typically use WireGuard for site-to-site connectivity (replacing public internet traversal) and Pomerium or Teleport for per-service, per-user access control on top of that. The WireGuard tunnel handles encryption and routing; the access proxy handles who can reach what.