WireGuard VPN Site-to-Site Deployment on Linux: Configuration, Key Management, and Hardening

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
WireGuard replaced an IPsec site-to-site VPN in a 12-site retail environment last year. The IPsec configuration had grown to a 400-line Strongswan config with IKEv2 proposals, pre-shared keys in a secrets file, three different DH groups for three different router firmware versions, and a spreadsheet tracking which site had which phase 1 and phase 2 lifetimes. The WireGuard replacement was 35 lines of configuration per site, deployed in an afternoon via Ansible, and has had zero tunnel renegotiation events since deployment because WireGuard does not negotiate — it simply establishes sessions using Noise protocol handshakes.
WireGuard's simplicity is not a euphemism for limitation. The cryptographic choices (ChaCha20-Poly1305, Curve25519 key exchange, BLAKE2s hashing) are modern and considered stronger than most IPsec cipher suite combinations in practice. The only meaningful operational differences from IPsec are that WireGuard is UDP-only (no TCP fallback), has no built-in certificate management (keys are static public/private pairs), and has no authentication header separation from encryption (all WireGuard packets are encrypted and authenticated together). These are acceptable tradeoffs for site-to-site and road-warrior use cases.
Core configuration: wg0.conf structure for site-to-site tunnels
WireGuard's configuration file is divided into two section types: [Interface] which describes the local WireGuard endpoint (private key, VPN IP address, listen port), and one [Peer] section for each remote peer (public key, allowed IP ranges, optional endpoint address). Every operational decision in a WireGuard deployment — routing, NAT traversal, full tunnel vs. split tunnel, automatic startup — flows from how these two section types are configured. Understanding what each directive does and what WireGuard does automatically without configuration is essential for building a working and secure deployment.
Generate keys on each endpoint and never move private keys across the network
Run wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key on each server that will participate in the WireGuard network and set chmod 600 /etc/wireguard/private.key immediately. The private key must never leave the server where it was generated — only the public key is shared with peer servers. The common mistake during initial setup is generating keys on a laptop and distributing them to servers via SCP or paste, which sends the private key over a network connection and potentially stores it in shell history. If a private key is ever transmitted over a network, treat it as compromised and regenerate. For infrastructure-as-code deployments with Ansible or Terraform, generate keys on the target server using Ansible's command module or a Terraform null_resource local-exec rather than generating them on the control plane and distributing.
Set MTU explicitly to 1420 to prevent fragmentation issues in WireGuard tunnels
WireGuard adds approximately 60 bytes of overhead per packet (20 bytes IPv4 header, 8 bytes UDP header, 32 bytes WireGuard header), so an internet MTU of 1500 bytes leaves a WireGuard MTU of approximately 1420 bytes for the encapsulated traffic. Without explicit MTU configuration, large packets that exceed 1420 bytes may be fragmented or silently dropped by intermediate network devices, causing intermittent connectivity failures that are difficult to diagnose. Add MTU = 1420 to the [Interface] section of wg0.conf and set the same MTU on the WireGuard interface on all peers. For environments where the outer network MTU is lower than 1500 (some ISPs use 1492 for PPPoE), calculate the WireGuard MTU as outer-MTU-minus-60 and set it accordingly.
Operations: peer management, monitoring, and key rotation
WireGuard's operational simplicity at initial setup becomes a management challenge at scale: with 20 sites and 100 road-warrior users, manually managing wg0.conf files on every peer creates drift between configurations and makes adding or removing peers error-prone. Key rotation is WireGuard's most commonly skipped operational practice — because WireGuard keys are static (unlike IPsec which renegotiates session keys automatically), the key material used today is the same material that was generated months or years ago and never rotated.
Use Ansible to manage WireGuard configurations and enforce configuration consistency across peers
Manage WireGuard configurations with Ansible by storing the wg0.conf template and peer public keys in a version-controlled Ansible inventory. The Ansible role generates the wg0.conf for each host by templating the [Interface] section from host-specific variables (private key path, VPN IP, listen port) and generating [Peer] sections from a list of all other peers in the inventory. When a new site is added, add its public key and VPN IP to the inventory and run the Ansible playbook, which updates wg0.conf on every existing peer and brings up the interface with wg-quick down wg0 && wg-quick up wg0. This approach ensures that every peer has a current, consistent configuration and that adding or removing a peer updates all affected configurations atomically rather than requiring manual edits on each server.
Schedule quarterly key rotation by regenerating key pairs and distributing new public keys before removing old ones
Rotate WireGuard keys quarterly by generating a new key pair on each peer, distributing the new public key to all other peers in a [Peer] section alongside the old key, bringing up the new configuration to verify connectivity with the new key, then removing the old key. The critical sequencing is to add the new public key as an additional [Peer] entry before removing the old one, so that connectivity is maintained throughout the rotation rather than having a brief outage between removing the old key and the new key taking effect. Document key rotation dates in the Ansible inventory or a configuration management database so that keys that have not been rotated in over a year can be identified and prioritized. For high-security environments, consider enabling pre-shared keys (PSK) in addition to the public key cryptography: adding PresharedKey to the [Peer] section adds a symmetric layer of cryptography that provides quantum-resistant protection for the session.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
The bottom line
WireGuard site-to-site deployment requires four things done correctly: key generation on the endpoint (never transmitted), wg0.conf peer configuration with explicit MTU 1420 and PersistentKeepalive 25 for NAT traversal, AllowedIPs configured for your routing model (specific CIDRs for split tunnel, 0.0.0.0/0 for full tunnel), and systemd service enablement for automatic startup. Add an iptables kill switch via PostUp/PreDown hooks to prevent traffic from bypassing the tunnel if it drops. Manage configurations with Ansible for consistency across multiple peers and schedule quarterly key rotation. Monitor tunnel health with last-handshake age using wg show or the prometheus-wireguard-exporter for Grafana alerting. WireGuard's operational simplicity is real — a working site-to-site tunnel between two Linux servers takes under an hour to configure from scratch.
Frequently asked questions
How do I configure a basic WireGuard site-to-site tunnel between two Linux servers?
Configure a WireGuard site-to-site tunnel by generating a key pair on each server and writing wg0.conf on both sides. On Server A (the initiator), generate keys with wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key and set permissions with chmod 600 /etc/wireguard/private.key. Write /etc/wireguard/wg0.conf with [Interface] section containing Address (the VPN IP for this server, e.g. 10.0.0.1/24), ListenPort 51820, and PrivateKey (from private.key). Add a [Peer] section with PublicKey (Server B's public key), AllowedIPs (Server B's VPN IP 10.0.0.2/32 plus Server B's LAN subnet e.g. 192.168.2.0/24), and Endpoint (Server B's public IP:51820). Repeat on Server B with reversed roles. Bring up the tunnel with wg-quick up wg0 on both servers and verify with wg show and ping 10.0.0.2 from Server A.
How does AllowedIPs control routing in WireGuard, and how do I configure split tunneling?
AllowedIPs in WireGuard serves two purposes simultaneously: it defines which source IP addresses are accepted from that peer (inbound filtering) and which destination IPs are routed through the tunnel to that peer (outbound routing). When wg-quick brings up the interface, it adds kernel routing table entries for each AllowedIPs CIDR pointing to the WireGuard interface. For full tunnel (route all traffic through the VPN), set AllowedIPs to 0.0.0.0/0 which installs a default route through the tunnel. For split tunneling (route only specific networks through the VPN), set AllowedIPs to the specific CIDRs you want routed through the tunnel (e.g. 10.0.0.0/8, 192.168.0.0/16) and traffic to other destinations uses the regular default route. When using 0.0.0.0/0 for full tunnel, add the Table = off option in the Interface section and manage routing manually if you need to prevent wg-quick from overriding your default route configuration.
How do I configure an iptables kill switch to prevent traffic leaking when the WireGuard tunnel drops?
Configure a kill switch using WireGuard's PostUp and PreDown hooks in wg0.conf to add and remove iptables rules that block all non-VPN traffic. In the [Interface] section, add PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT which blocks all output traffic that does not leave through the WireGuard interface (wg0) and is not marked as WireGuard internal traffic. Add PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT to remove the rule when the tunnel comes down. This kill switch ensures that if the WireGuard tunnel drops unexpectedly, the applications using it receive connection errors rather than silently routing through the unencrypted internet connection. Add IPv6 equivalents using ip6tables if the host has IPv6 connectivity.
How do I add multiple peers for a hub-and-spoke or mesh WireGuard configuration?
Add multiple peers to a WireGuard hub-and-spoke configuration by adding a [Peer] section for each spoke on the hub server. Each spoke peer section contains the spoke's public key, a unique VPN IP in AllowedIPs (e.g. 10.0.0.2/32 for spoke 1, 10.0.0.3/32 for spoke 2), and the spoke's endpoint if the spoke has a static public IP. For spoke servers that are behind NAT without a static IP, omit the Endpoint on the hub and add PersistentKeepalive = 25 on the spoke so the spoke initiates and maintains the connection. For a mesh topology where all nodes communicate directly without going through a hub, each node's wg0.conf contains a [Peer] section for every other node in the mesh. Use a configuration management tool (Ansible, Puppet) or a WireGuard management layer (wg-easy, Netmaker, Headscale) when managing more than five peers, as the manual configuration becomes error-prone at scale.
How do I troubleshoot a WireGuard tunnel that is not passing traffic?
Troubleshoot WireGuard connectivity by working through the OSI layers. First check that the WireGuard interface is up and the configuration is loaded with wg show which displays the interface, listening port, each peer's public key, allowed IPs, and the handshake timestamp and data transfer counts. If the handshake timestamp shows more than two minutes ago, the tunnel is not maintaining a live connection — check that UDP port 51820 is open in both directions using nc -u server-b-ip 51820 from Server A. If the handshake completes (recent timestamp) but traffic is not flowing, check the AllowedIPs configuration: a packet to 192.168.2.5 will not be sent through the tunnel if AllowedIPs does not include 192.168.2.0/24. Run tcpdump -i wg0 on the destination server to see if packets are arriving at the WireGuard interface. Check that IP forwarding is enabled on servers that need to forward traffic between the tunnel and the LAN: sysctl net.ipv4.ip_forward should return 1.
How do I enable WireGuard to start automatically with systemd?
Enable automatic WireGuard startup with systemd by enabling the wg-quick service for the specific interface: systemctl enable wg-quick@wg0.service which creates a systemd unit that runs wg-quick up wg0 at boot and wg-quick down wg0 at shutdown. Start the service immediately with systemctl start wg-quick@wg0.service and check status with systemctl status wg-quick@wg0.service. The service logs to the system journal, viewable with journalctl -u wg-quick@wg0 -f. For systems using systemd-networkd instead of wg-quick, create a .netdev file in /etc/systemd/network/ with the WireGuard configuration in a [WireGuard] section and [WireGuardPeer] sections for each peer, then create a corresponding .network file that assigns the IP address. The systemd-networkd approach integrates WireGuard into the standard network configuration framework and is preferable on servers where systemd-networkd already manages network interfaces.
How do I monitor WireGuard tunnel health and detect when a peer disconnects?
Monitor WireGuard tunnel health using the last handshake timestamp from wg show, which shows when the most recent successful cryptographic handshake occurred with each peer. WireGuard performs a handshake every 180 seconds when traffic is flowing and when PersistentKeepalive fires, so a handshake timestamp older than five minutes indicates the peer is unreachable. Script a monitor using wg show wg0 dump which outputs tab-separated peer data including the handshake timestamp as a Unix timestamp. In a monitoring script, compare the current timestamp against the last handshake and alert if the difference exceeds 300 seconds. For Prometheus-based monitoring, use the prometheus-wireguard-exporter which exposes wireguard_peer_last_handshake_seconds metrics for each peer that Grafana can display as a panel and alert on. Configure Grafana to alert when any peer's last handshake exceeds 300 seconds, which indicates a tunnel outage requiring investigation.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
