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 with sudo apt install qrencode (or sudo 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 51820 from 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 -v and 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 = 1380 to 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.1 to 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.conf while the tunnel is up and verify it points to the correct DNS servers. You may need to install the openresolv or resolvconf package.

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 genpsk and add PresharedKey = PSK_VALUE to 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-handshakes and 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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here