WireGuard has become the go-to VPN protocol for engineers who value speed, simplicity, and strong cryptography. Unlike OpenVPN or IPsec, WireGuard lives inside the Linux kernel, which means less overhead, fewer lines of code to audit, and faster tunnel negotiation. This guide walks you through a full WireGuard deployment on Ubuntu 24.04, Debian 13, and Rocky Linux 10, covering everything from basic point-to-point tunnels to site-to-site configurations and DNS leak prevention.
What Is WireGuard?
WireGuard is a modern VPN protocol that operates at the kernel level on Linux systems. The entire codebase sits around 4,000 lines of code, compared to the hundreds of thousands found in OpenVPN or IPsec implementations like StrongSwan. That small footprint makes it easier to audit, easier to maintain, and significantly harder to introduce security bugs.
From a performance standpoint, WireGuard consistently outperforms both OpenVPN and IPsec in throughput benchmarks. It uses modern cryptographic primitives: Curve25519 for key exchange, ChaCha20 for symmetric encryption, Poly1305 for authentication, and BLAKE2s for hashing. There is no cipher negotiation. Every peer uses the same set of algorithms, which eliminates an entire class of downgrade attacks.
WireGuard operates as a Layer 3 tunnel and presents itself as a standard network interface (wg0, wg1, etc.). Configuration is minimal. A typical server config file is under 15 lines. If you have spent hours wrestling with OpenVPN certificate chains or IPsec Phase 1/Phase 2 policies, WireGuard will feel like a breath of fresh air.
Prerequisites
Before you start, make sure you have the following in place:
- A server running Ubuntu 24.04, Debian 13 (Trixie), or Rocky Linux 10 / AlmaLinux 10 with root or sudo access
- A public IP address on the server (or a known NAT mapping if the server sits behind a firewall)
- UDP port 51820 open on any upstream firewalls or cloud security groups
- At least one client device for testing
Step 1: Install WireGuard
Ubuntu 24.04 and Debian 13
WireGuard ships in the default repositories on both Ubuntu 24.04 and Debian 13. Install it with apt:
sudo apt update && sudo apt install wireguard wireguard-tools -y
The wireguard package pulls in the kernel module (already built into kernels 5.6 and later), while wireguard-tools provides the wg and wg-quick utilities you will use for key generation and tunnel management.
Rocky Linux 10 and AlmaLinux 10
On RHEL-based distributions, WireGuard is available through the EPEL repository. Enable EPEL first, then install:
sudo dnf install epel-release -y
sudo dnf install wireguard-tools -y
Rocky Linux 10 and AlmaLinux 10 ship with kernel 6.x, which includes the WireGuard module by default. You do not need to install a separate DKMS package.
Confirm the module is available on any distribution by running:
sudo modprobe wireguard && echo "WireGuard module loaded"
Step 2: Generate Server Keys
WireGuard uses asymmetric key pairs, similar in concept to SSH keys. Each peer (server or client) has a private key and a corresponding public key. Generate the server key pair now:
wg genkey | sudo tee /etc/wireguard/server_private.key | wg pubkey | sudo tee /etc/wireguard/server_public.key
Lock down the private key so only root can read it:
sudo chmod 600 /etc/wireguard/server_private.key
View the keys for use in the configuration file:
cat /etc/wireguard/server_private.key
cat /etc/wireguard/server_public.key
Keep the private key secret. Never share it, never commit it to version control, and never paste it into a chat window. The public key is safe to distribute to any peer that needs to connect.
Step 3: Configure the WireGuard Server
Create the server configuration file at /etc/wireguard/wg0.conf. Replace SERVER_PRIVATE_KEY with the actual contents of your private key, and adjust the network interface name in the PostUp/PostDown rules to match your server (commonly eth0, ens3, or enp1s0).
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
Here is what each directive does:
- Address: The VPN IP address assigned to the server interface. The /24 subnet gives you room for up to 254 peers.
- ListenPort: The UDP port WireGuard listens on. 51820 is the default, but you can use any available port.
- PrivateKey: The server’s private key generated in the previous step.
- PostUp: Firewall rules that run when the tunnel comes up. The FORWARD rule allows traffic through the tunnel, and the MASQUERADE rule performs NAT so tunnel traffic can reach the internet through the server’s public interface.
- PostDown: Cleans up those same firewall rules when the tunnel goes down.
Set proper permissions on the configuration file:
sudo chmod 600 /etc/wireguard/wg0.conf
Step 4: Enable IP Forwarding
For the server to route traffic between VPN clients and the outside world, you must enable IP forwarding. Without this, packets that arrive on the wg0 interface will be dropped instead of forwarded to the public interface.
Enable it immediately and persist the setting across reboots:
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Verify that forwarding is active:
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
Both values should return 1.
Step 5: Start WireGuard
Bring up the tunnel using wg-quick:
sudo wg-quick up wg0
You should see output confirming the interface was created and the routes were added. Verify the interface is running:
sudo wg show wg0
Enable the tunnel to start automatically at boot:
sudo systemctl enable wg-quick@wg0
If you ever need to bring the tunnel down for maintenance, run:
sudo wg-quick down wg0
Step 6: Configure Firewall Rules
UFW (Ubuntu and Debian)
If you use UFW on Ubuntu or Debian, allow the WireGuard port and enable forwarding:
sudo ufw allow 51820/udp
sudo ufw allow OpenSSH
sudo ufw enable
You also need to edit /etc/default/ufw and change DEFAULT_FORWARD_POLICY from DROP to ACCEPT:
DEFAULT_FORWARD_POLICY="ACCEPT"
Restart UFW after making this change:
sudo ufw reload
firewalld (Rocky Linux and AlmaLinux)
On Rocky Linux and AlmaLinux, use firewall-cmd to open the port and enable masquerading:
sudo firewall-cmd --add-port=51820/udp --permanent
sudo firewall-cmd --add-masquerade --permanent
sudo firewall-cmd --reload
Confirm the rules are in place:
sudo firewall-cmd --list-all
Step 7: Generate Client Keys and Configuration
Each client needs its own key pair. You can generate these on the server or on the client machine itself. Generating on the server is convenient for centralized management, but generating on the client means the private key never crosses the network.
Generate a client key pair:
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
Now create the client configuration file. Replace CLIENT_PRIVATE_KEY with the client’s private key, SERVER_PUBLIC_KEY with the server’s public key, and SERVER_PUBLIC_IP with your server’s actual public IP address.
[Interface]
Address = 10.10.0.2/32
PrivateKey = CLIENT_PRIVATE_KEY
DNS = 1.1.1.1, 9.9.9.9
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = SERVER_PUBLIC_IP:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
A few notes on this configuration:
- Address: The client’s VPN IP. Use /32 here because this is a single host, not a subnet.
- DNS: The DNS servers the client will use when the tunnel is active. Using public resolvers like Cloudflare (1.1.1.1) or Quad9 (9.9.9.9) keeps things simple.
- AllowedIPs: Setting this to 0.0.0.0/0 routes all traffic through the VPN (full tunnel). For split tunnel, specify only the subnets you want to reach through the VPN, for example
10.10.0.0/24, 192.168.1.0/24. - PersistentKeepalive: Sends a keepalive packet every 25 seconds. This is necessary when the client sits behind NAT, which is the case for most home and mobile connections.
Step 8: Add the Peer to the Server Configuration
Back on the server, add a [Peer] block to /etc/wireguard/wg0.conf for each client. You need the client’s public key and the VPN IP you assigned to it.
[Peer]
PublicKey = CLIENT1_PUBLIC_KEY
AllowedIPs = 10.10.0.2/32
You can add peers without restarting the tunnel by using the wg command directly:
sudo wg set wg0 peer CLIENT1_PUBLIC_KEY allowed-ips 10.10.0.2/32
To persist this change, save the running configuration back to the file:
sudo wg-quick save wg0
For additional clients, repeat Steps 7 and 8 with unique IP addresses (10.10.0.3, 10.10.0.4, and so on).
Step 9: Client Setup by Platform
Linux
Install WireGuard using your distribution’s package manager (same process as the server install). Copy the client configuration file to /etc/wireguard/wg0.conf, then bring up the tunnel:
sudo wg-quick up wg0
Enable it at boot with:
sudo systemctl enable wg-quick@wg0
macOS
Install the WireGuard app from the Mac App Store, or use Homebrew:
brew install wireguard-tools
If you use the GUI app, click “Import tunnel(s) from file” and select your .conf file. If you prefer the command line, place the config at /usr/local/etc/wireguard/wg0.conf and use wg-quick up wg0.
Windows
Download the official WireGuard client from wireguard.com/install. After installation, click “Import tunnel(s) from file” and select your .conf file, or click “Add empty tunnel” and paste the configuration directly. Click “Activate” to connect.
iOS and Android
Install the WireGuard app from the App Store or Google Play. You have two options for importing the configuration:
- QR code: Generate a QR code from the config file on the server using
qrencode. Install it withsudo apt install qrencode(orsudo dnf install qrencode), then run:qrencode -t ansiutf8 < client1.conf. Point your phone's WireGuard app at the terminal to scan it. - File import: Transfer the .conf file to your device and open it with the WireGuard app.
Step 10: Verify the Connection
Once the client tunnel is up, run these checks to confirm everything works.
On the server, check the WireGuard interface status:
sudo wg show
You should see the peer listed with a recent handshake timestamp and non-zero transfer counters. If the "latest handshake" field is missing, the client has not successfully connected yet.
From the client, ping the server's VPN IP:
ping -c 4 10.10.0.1
Run a traceroute to confirm traffic flows through the tunnel:
traceroute 10.10.0.1
If you configured a full tunnel (AllowedIPs = 0.0.0.0/0), verify your public IP has changed by checking from the client:
curl -s https://ifconfig.me
The returned IP should be the server's public IP, not the client's original address.
Step 11: Site-to-Site VPN Configuration
WireGuard works well for connecting two networks together, such as linking an office LAN (192.168.1.0/24) to a cloud VPC (172.16.0.0/24). In a site-to-site setup, both sides act as peers, and each side advertises its local subnet through the AllowedIPs directive.
Site A Configuration (Office Gateway)
The office gateway has a LAN of 192.168.1.0/24 and a WireGuard VPN IP of 10.10.0.1:
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = SITE_A_PRIVATE_KEY
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = SITE_B_PUBLIC_KEY
Endpoint = SITE_B_PUBLIC_IP:51820
AllowedIPs = 10.10.0.2/32, 172.16.0.0/24
PersistentKeepalive = 25
Site B Configuration (Cloud Gateway)
The cloud gateway has a VPC subnet of 172.16.0.0/24 and a WireGuard VPN IP of 10.10.0.2:
[Interface]
Address = 10.10.0.2/24
ListenPort = 51820
PrivateKey = SITE_B_PRIVATE_KEY
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i 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.10.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25
The critical piece here is the AllowedIPs on each side. Site A lists Site B's local subnet (172.16.0.0/24) so it knows to route that traffic into the tunnel. Site B does the same for Site A's subnet (192.168.1.0/24). Both sides need IP forwarding enabled and their local firewalls configured to allow forwarding between the WireGuard interface and the LAN interface.
For hosts on each LAN to reach the remote network, they need a route pointing to their local WireGuard gateway. You can add static routes on individual hosts or, more practically, add the route on your default gateway or DHCP server so all LAN clients pick it up automatically.
On a Linux router at Site A, for example:
sudo ip route add 172.16.0.0/24 via 192.168.1.1 dev eth0
Replace 192.168.1.1 with the LAN IP of the machine running WireGuard at Site A.
Step 12: DNS Leak Prevention
A DNS leak occurs when your system sends DNS queries outside the VPN tunnel, revealing the sites you visit to your ISP or local network operator. This defeats much of the privacy benefit of running a VPN.
WireGuard's client configuration includes a DNS directive that tells wg-quick to set the system's DNS servers when the tunnel comes up. Make sure this is set in your client config:
DNS = 1.1.1.1, 9.9.9.9
On Linux clients using systemd-resolved, wg-quick automatically configures the DNS through resolvectl. You can verify it is working correctly:
resolvectl status wg0
This should show your configured DNS servers associated with the wg0 interface.
For stronger leak prevention, consider these additional measures:
- Run your own DNS resolver on the WireGuard server. Install Unbound or dnsmasq, bind it to the VPN interface (10.10.0.1), and set the client DNS directive to that address. This keeps all DNS traffic inside the tunnel.
- Block DNS traffic that bypasses the tunnel. On the client, add iptables rules that drop any DNS traffic (port 53) not destined for your VPN DNS server.
- Use DNS over TLS or DNS over HTTPS. If your DNS resolver supports it, encrypted DNS adds another layer of protection even if a leak occurs.
To test for DNS leaks, connect to your VPN and visit a DNS leak test site such as dnsleaktest.com. Run the extended test and verify that only your expected DNS servers appear in the results.
Step 13: Troubleshooting Common Issues
Handshake Timeout
If wg show on the server never shows a handshake for a peer, the initial key exchange is failing. Common causes:
- Firewall blocking UDP 51820. Verify the port is open on the server, any cloud security groups, and any intermediate firewalls. Test with
nc -u -z SERVER_IP 51820from the client side. - Wrong endpoint in client config. Double-check the server's public IP and port in the client's [Peer] Endpoint field.
- Mismatched keys. The server's [Peer] PublicKey must match the client's actual public key, and the client's [Peer] PublicKey must match the server's actual public key. A single character off will cause a silent handshake failure.
- Clock skew. WireGuard uses timestamps to prevent replay attacks. If the server and client clocks differ by more than a few minutes, handshakes can fail. Ensure NTP is running on both sides.
Handshake Succeeds but No Traffic Flows
If you see a valid handshake but cannot pass traffic through the tunnel, check these areas:
- IP forwarding not enabled. Confirm with
sysctl net.ipv4.ip_forward. It must return 1. - PostUp iptables rules not applied. Run
sudo iptables -t nat -L POSTROUTING -vand look for the MASQUERADE rule on your public interface. If it is missing, check that the interface name in your PostUp line matches the actual interface. - AllowedIPs mismatch. The AllowedIPs on the server must include the client's VPN IP. On the client, AllowedIPs determines what traffic enters the tunnel. If you set it to 10.10.0.0/24 but try to reach the internet through the VPN, that traffic will not be routed into the tunnel.
- MTU issues. WireGuard sets a default MTU of 1420. If you are running WireGuard inside another tunnel or over a link with a low MTU, you may need to lower it. Add
MTU = 1380to the [Interface] section of both server and client configs.
DNS Resolution Fails
If you can ping IP addresses through the tunnel but cannot resolve hostnames:
- DNS directive missing from client config. Add
DNS = 1.1.1.1to the [Interface] section. - systemd-resolved not picking up the change. Restart systemd-resolved:
sudo systemctl restart systemd-resolved. Then bring the tunnel down and back up. - Client firewall blocking DNS. If the client has strict outbound rules, make sure port 53 (or 853 for DNS over TLS) is allowed to the configured DNS servers.
- /etc/resolv.conf not updated. On some systems, wg-quick cannot update the resolver configuration automatically. Check
/etc/resolv.confwhile the tunnel is up and verify it points to the correct DNS servers. You may need to install theopenresolvorresolvconfpackage.
Debugging with Kernel Logs
WireGuard operates in kernel space, so its log output goes to the kernel ring buffer. Enable dynamic debug for more verbose logging:
echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control
Then watch the output with:
sudo dmesg -wT | grep wireguard
Remember to disable debug logging when you are done, as it can generate a lot of output on busy servers:
echo module wireguard -p | sudo tee /sys/kernel/debug/dynamic_debug/control
Security Hardening Tips
Beyond the basic setup, consider these practices to tighten your WireGuard deployment:
- Restrict SSH to the VPN. Once WireGuard is running, configure sshd to listen only on the VPN IP (10.10.0.1) or use firewall rules to limit SSH access to the VPN subnet. This removes SSH from your public attack surface entirely.
- Use a preshared key. WireGuard supports an optional preshared key (PSK) that adds a layer of symmetric encryption on top of the Curve25519 key exchange. This provides post-quantum resistance. Generate one with
wg genpskand addPresharedKey = PSK_VALUEto both the server and client [Peer] blocks. - Rotate keys periodically. While WireGuard's key exchange is cryptographically sound, rotating keys every few months limits the window of exposure if a key is compromised.
- Monitor peer activity. Script a cron job that runs
wg show wg0 latest-handshakesand alerts you if a peer has not connected in an expected timeframe, which could indicate a compromised or decommissioned device. - Keep the server updated. WireGuard is part of the kernel, so regular kernel updates include WireGuard fixes. Do not skip them.
Managing Multiple Clients
As your deployment grows, maintaining the wg0.conf file manually becomes tedious. Here are some practical approaches for managing larger deployments:
Keep a spreadsheet or internal wiki page that maps each client's VPN IP, public key, and the user or device it belongs to. When someone leaves the team or a device is lost, remove their [Peer] block and restart the tunnel.
To remove a peer without restarting:
sudo wg set wg0 peer CLIENT_PUBLIC_KEY remove
Then save the running config:
sudo wg-quick save wg0
For teams that need a web interface, tools like Wireguard-UI or wg-access-server provide a browser-based dashboard for peer management, but evaluate them carefully before deploying in production since they add attack surface.
Conclusion
WireGuard gives you a production-grade VPN with minimal configuration overhead. The setup covered here works across Ubuntu 24.04, Debian 13, and Rocky Linux 10 / AlmaLinux 10, whether you need a simple remote access VPN for a small team or a site-to-site tunnel linking two networks. The protocol is fast, the configuration is straightforward, and the cryptographic foundation is solid. Keep your keys secure, your firewall rules tight, and your kernel patched, and WireGuard will serve you well.


































































