Install and Configure WireGuard on Ubuntu Server 24.04

A WireGuard story that starts small and gets serious fast

At first, remote access is simple. We SSH into a server from home, maybe open a port, maybe add a quick firewall rule, and move on. Then the environment grows. A second admin joins. A laptop gets replaced. A contractor needs temporary access. A branch office appears. Suddenly, “just open a port” becomes a long-lived risk, and every exception becomes a permanent liability.

This is where WireGuard earns its place. It gives us a small, auditable VPN surface area, modern cryptography, and a configuration model that stays readable even when the number of peers grows. In this guide, we will deploy WireGuard on Ubuntu Server 24.04 as the VPN server, then integrate Linux clients (RHEL/Alma) and Windows clients in a way that holds up in home labs, professional teams, and enterprise change control.

Prerequisites and assumptions we are building on

Before we touch commands, we need to be explicit about the environment. WireGuard is forgiving, but production environments are not.

  • Server OS: Ubuntu Server 24.04 LTS, fresh deployment (no prior VPN tooling assumed).
  • Access: We have shell access with a user that can run sudo. We will not run as root interactively unless needed.
  • Network: The server has a public IPv4 address or is reachable via port-forwarding from the internet. If we are behind NAT, we must forward UDP port 51820 to the server.
  • Firewall: We will use UFW on Ubuntu. If UFW is not desired, we must implement equivalent nftables/iptables rules. We will keep the rules minimal and explicit.
  • VPN addressing: We will use 10.10.10.0/24 for the VPN. The server will be 10.10.10.1. Each client gets a unique /32.
  • DNS: We will optionally push a DNS resolver to clients. In many environments, we point clients to the VPN server itself (if it runs DNS) or to a trusted internal resolver. In this guide, we will use public resolvers as a safe default and call out where to change it.
  • Security posture: We will keep private keys readable only by root, avoid broad file permissions, and ensure the service starts on boot.

Plan the deployment: what we are building and why

We are going to build a hub-and-spoke VPN:

  • The Ubuntu 24.04 server is the hub. It listens on UDP/51820.
  • Clients (Ubuntu/Debian, RHEL/Alma, Windows) connect as peers.
  • We will enable IP forwarding on the server so VPN clients can reach other networks through it if needed.
  • We will apply firewall rules that allow WireGuard traffic and allow forwarding from the VPN interface to the internet (common for “full tunnel” or “internet breakout”). If we only need access to the server itself, we can skip NAT and keep forwarding tighter.

Ubuntu Server 24.04: install WireGuard and prepare the system

Step 1: Confirm OS version and baseline network facts

We will first confirm we are on the expected OS and capture the server’s public-facing IP and default outbound interface. This matters because firewall/NAT rules must reference the correct interface, and client configs must point to the correct endpoint.

lsb_release -a
ip -br addr
ip route show default

We have now confirmed the OS release and seen the server’s IP addresses and default route. The default route output will show the outbound interface (for example, eth0) which we will use later for NAT.

Step 2: Install WireGuard and supporting tools

We will install WireGuard and a few baseline tools. WireGuard on Ubuntu uses the in-kernel implementation, so we are not pulling in heavy dependencies. We include qrencode only if we later want to generate QR codes for mobile clients; it is optional, but harmless and often useful.

sudo apt-get update
sudo apt-get install -y wireguard resolvconf ufw qrencode

WireGuard and UFW are now installed. We also installed resolvconf to help clients manage DNS cleanly when interfaces come up and down.

Step 3: Enable IP forwarding for routed VPN traffic

We will enable IPv4 forwarding so the server can route traffic between the WireGuard interface and other networks. Without this, clients may connect successfully but fail to reach anything beyond the server itself.

sudo sysctl -w net.ipv4.ip_forward=1
sudo bash -c 'printf "n# WireGuard routingnnet.ipv4.ip_forward=1n" >> /etc/sysctl.conf'
sudo sysctl -p | grep -E 'net.ipv4.ip_forward'

Forwarding is now enabled immediately and persisted across reboots via /etc/sysctl.conf. The final command confirms the active value.

Ubuntu Server 24.04: generate keys and build the server configuration

Step 4: Create a secure directory for WireGuard and generate server keys

We will create the WireGuard configuration directory (if it does not exist) and generate a server private/public key pair. Private keys must be readable only by root. This is non-negotiable in production.

sudo install -d -m 700 /etc/wireguard
sudo bash -c 'umask 077; wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub'
sudo ls -l /etc/wireguard
sudo cat /etc/wireguard/server.pub

We now have server.key (private) and server.pub (public). The directory permissions and umask ensure the private key is not exposed to non-root users.

Step 5: Detect the outbound interface and set safe shell variables

We will capture the default outbound interface into a variable. This keeps later firewall/NAT commands copy/paste-safe without guessing interface names.

EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
echo "Outbound interface: ${EXT_IFACE}"

We now have EXT_IFACE set for this shell session. We will use it to build NAT rules that survive reboots via WireGuard’s interface hooks.

Step 6: Create the WireGuard server configuration

We will create /etc/wireguard/wg0.conf. This file defines the server interface, its VPN IP, the listening port, and firewall/NAT hooks. We are using PostUp/PostDown so the rules are applied only when the VPN is up, which keeps the system predictable.

sudo bash -c 'cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.10.10.1/24
ListenPort = 51820
PrivateKey = '"$(sudo cat /etc/wireguard/server.key)"'

# Minimal, explicit forwarding + NAT for VPN clients
# These rules are applied when wg0 comes up and removed when it goes down.
PostUp = ufw route allow in on wg0 out on '"${EXT_IFACE}"'
PostUp = ufw route allow in on '"${EXT_IFACE}"' out on wg0
PostUp = iptables -t nat -A POSTROUTING -o '"${EXT_IFACE}"' -s 10.10.10.0/24 -j MASQUERADE
PostDown = ufw route delete allow in on wg0 out on '"${EXT_IFACE}"'
PostDown = ufw route delete allow in on '"${EXT_IFACE}"' out on wg0
PostDown = iptables -t nat -D POSTROUTING -o '"${EXT_IFACE}"' -s 10.10.10.0/24 -j MASQUERADE
EOF'

We now have a complete server interface definition. The server will own 10.10.10.1, listen on UDP/51820, and apply routing/NAT rules only while wg0 is active.

Step 7: Lock down configuration permissions

We will ensure only root can read the WireGuard configuration. This file contains the server private key, so it must not be world-readable.

sudo chown root:root /etc/wireguard/wg0.conf
sudo chmod 600 /etc/wireguard/wg0.conf
sudo ls -l /etc/wireguard/wg0.conf

The configuration is now protected with strict permissions.

Ubuntu Server 24.04: firewall and service enablement

Step 8: Configure UFW to allow WireGuard and forwarding

We will allow inbound UDP/51820 and enable routed traffic. We will also enable UFW if it is not already active. This step is where many “it connects but nothing works” issues begin, so we will verify carefully.

sudo ufw allow 51820/udp
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw status verbose

We have now allowed WireGuard’s UDP port and set sane defaults. The status output confirms the active rules.

Next, we will enable UFW if it is not enabled yet. On a remote server, enabling a firewall can lock us out if SSH is not allowed. We will explicitly allow SSH first.

sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw status verbose

UFW is now enabled and will persist across reboots. SSH remains allowed, and WireGuard UDP/51820 is open.

Step 9: Start WireGuard and enable it on boot

We will bring up the WireGuard interface using wg-quick and enable the systemd unit so it starts automatically after reboots. This is essential for production reliability.

sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager

The WireGuard interface is now active and managed by systemd. The status output confirms whether the service is running and if any configuration errors occurred.

Step 10: Verify the server is listening and the interface exists

We will verify that the server is listening on UDP/51820 and that the WireGuard interface is up. This confirms the service is operational before we add clients.

sudo ss -lunp | grep -E ':51820'
sudo wg show
ip -br addr show wg0

We should see a UDP listener on port 51820, a wg0 interface with 10.10.10.1/24, and an empty peer list for now.

Add the first peer: generate a client profile on the server

Step 11: Generate keys for a client and reserve an IP

We will generate a client key pair on the server and assign a dedicated VPN IP. In production, we typically store peer material in a secure vault and automate provisioning, but for a fresh deployment this approach is controlled and auditable.

CLIENT_NAME="client1"
CLIENT_IP="10.10.10.2/32"

sudo bash -c 'umask 077; wg genkey | tee /etc/wireguard/'"${CLIENT_NAME}"'.key | wg pubkey > /etc/wireguard/'"${CLIENT_NAME}"'.pub'

sudo cat /etc/wireguard/${CLIENT_NAME}.pub

We now have a client private key and public key stored on the server. The public key will be used in the server configuration to define the peer.

Step 12: Add the client as a peer on the server

We will append a peer block to wg0.conf and then reload the interface. This tells the server to accept traffic from the client’s public key and route the client’s VPN IP.

CLIENT_PUBKEY=$(sudo cat /etc/wireguard/${CLIENT_NAME}.pub)

sudo bash -c 'cat >> /etc/wireguard/wg0.conf <<EOF

[Peer]
# ${CLIENT_NAME}
PublicKey = '"${CLIENT_PUBKEY}"'
AllowedIPs = '"${CLIENT_IP}"'
EOF'

sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
sudo wg show

The server now knows about the peer and will accept packets from that public key. The wg show output should list the peer, though the handshake will remain empty until the client connects.

Step 13: Build the client configuration file content

We will assemble a client configuration that includes the client private key, the server public key, and the server endpoint. We will first detect the server’s public IP as seen from the internet. If the server is behind NAT, we must use the public IP of the router and ensure UDP/51820 is forwarded to the server.

SERVER_PUBKEY=$(sudo cat /etc/wireguard/server.pub)
SERVER_PUBLIC_IP=$(curl -4 -s https://ifconfig.me || true)
echo "Detected public IP: ${SERVER_PUBLIC_IP}"
echo "Server public key: ${SERVER_PUBKEY}"

We now have the server public key and a best-effort detected public IP. If the detected IP is empty or incorrect (common behind strict egress rules), we must set the correct public IP or DNS name manually in the client config.

Next, we will print a complete client configuration to the terminal so we can copy it into the appropriate client platform. We will use a conservative DNS setting and a keepalive to help NATed clients stay connected.

CLIENT_PRIVKEY=$(sudo cat /etc/wireguard/${CLIENT_NAME}.key)

cat <<EOF
[Interface]
PrivateKey = ${CLIENT_PRIVKEY}
Address = 10.10.10.2/32
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = ${SERVER_PUBKEY}
Endpoint = ${SERVER_PUBLIC_IP}:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF

This output is the full client profile. With AllowedIPs = 0.0.0.0/0, the client will send all traffic through the VPN (full tunnel). If we only want access to internal networks, we will narrow AllowedIPs later.

Ubuntu/Debian client integration

Install WireGuard on Ubuntu/Debian clients

On Ubuntu/Debian clients, we will install WireGuard and ensure we can bring the interface up reliably. We do this first so that when we place the configuration file, the system can immediately validate it.

sudo apt-get update
sudo apt-get install -y wireguard resolvconf

The client now has WireGuard installed and can manage DNS updates cleanly when the VPN connects.

Apply the client configuration and start the tunnel

We will place the client configuration into /etc/wireguard/wg0.conf with strict permissions, then start and enable the service. This ensures persistence across reboots.

sudo install -d -m 700 /etc/wireguard
sudo bash -c 'cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
PrivateKey = REPLACE_WITH_CLIENT_PRIVATE_KEY
Address = 10.10.10.2/32
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = REPLACE_WITH_SERVER_PUBLIC_KEY
Endpoint = REPLACE_WITH_SERVER_PUBLIC_IP_OR_DNS:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF'
sudo chmod 600 /etc/wireguard/wg0.conf

The file is now in place, but it contains explicit placeholders. We keep placeholders out of commands whenever possible, but client private keys and endpoint values are inherently environment-specific. We will now replace them safely using an editor so we do not accidentally corrupt the file with shell escaping.

sudo nano /etc/wireguard/wg0.conf

After saving the correct values, we will start the interface and enable it on boot.

sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip -br addr show wg0

The client should now show an active wg0 interface and a peer entry. If the server is reachable, we will see a recent handshake timestamp.

Verify end-to-end connectivity from the client

We will verify that the client can reach the server’s VPN IP and that traffic is flowing.

ping -c 3 10.10.10.1
sudo wg show

If ping succeeds and wg show shows increasing transfer counters, the tunnel is working.

RHEL / AlmaLinux client integration

Install WireGuard tools on RHEL/AlmaLinux

On RHEL-family systems, WireGuard support is typically provided via kernel support and userland tools. We will install the tools using the system package manager. The exact repository availability can vary by version, so we will verify the package presence after installation.

sudo dnf install -y wireguard-tools

The system now has wg and wg-quick available. Next we will place the configuration and manage it with systemd for persistence.

Create the client configuration and secure it

We will create /etc/wireguard/wg0.conf and lock it down. As with Ubuntu, we will edit in the client private key, server public key, and endpoint.

sudo install -d -m 700 /etc/wireguard
sudo bash -c 'cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
PrivateKey = REPLACE_WITH_CLIENT_PRIVATE_KEY
Address = 10.10.10.2/32
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = REPLACE_WITH_SERVER_PUBLIC_KEY
Endpoint = REPLACE_WITH_SERVER_PUBLIC_IP_OR_DNS:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF'
sudo chmod 600 /etc/wireguard/wg0.conf

The configuration file exists with secure permissions. We will now edit it to insert the correct values.

sudo vi /etc/wireguard/wg0.conf

After saving, we will enable and start the service.

sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip -br addr show wg0

The RHEL/Alma client should now show an active interface and a peer with a handshake once connectivity is established.

Verify connectivity from the RHEL/Alma client

We will confirm reachability to the server’s VPN IP and confirm that WireGuard counters increase.

ping -c 3 10.10.10.1
sudo wg show

If ping succeeds and transfer counters move, the client is successfully integrated.

Windows client integration

Install WireGuard for Windows and prepare a clean client profile

On Windows, WireGuard is managed through the official WireGuard application. The key operational detail is that Windows will enforce routes based on AllowedIPs, and DNS behavior depends on the interface settings in the client profile.

We will create a new tunnel in the WireGuard Windows app and paste a configuration that matches the server’s peer entry. We will generate a unique key pair on Windows (recommended) or reuse the server-generated client keys (acceptable for controlled environments). For production hygiene, generating keys on the endpoint is often preferred because private keys never leave the device.

Windows configuration example (paste into the WireGuard app)

We will paste the following structure into the Windows WireGuard client. We must replace the placeholders with real values: the client private key generated on Windows, the server public key from /etc/wireguard/server.pub, and the server’s public IP/DNS.

[Interface]
PrivateKey = REPLACE_WITH_WINDOWS_CLIENT_PRIVATE_KEY
Address = 10.10.10.3/32
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = REPLACE_WITH_SERVER_PUBLIC_KEY
Endpoint = REPLACE_WITH_SERVER_PUBLIC_IP_OR_DNS:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Once we save and activate the tunnel, Windows will create a WireGuard adapter and apply routes. If the endpoint is reachable, the client will establish a handshake within seconds.

Add the Windows peer to the Ubuntu server

Because Windows generated its own keys, we must add its public key to the server. We will do this by appending a new peer block and reloading the configuration. We will assign 10.10.10.3/32 to match the Windows profile.

WIN_PEER_NAME="windows1"
WIN_PEER_PUBKEY="REPLACE_WITH_WINDOWS_CLIENT_PUBLIC_KEY"
WIN_PEER_IP="10.10.10.3/32"

sudo bash -c 'cat >> /etc/wireguard/wg0.conf <<EOF

[Peer]
# '"${WIN_PEER_NAME}"'
PublicKey = '"${WIN_PEER_PUBKEY}"'
AllowedIPs = '"${WIN_PEER_IP}"'
EOF'

sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
sudo wg show

The server now recognizes the Windows client. When the Windows tunnel is activated, wg show on the server should display a handshake and transfer counters for that peer.

Verify from the server that Windows is connected

We will verify the handshake and traffic counters from the Ubuntu server side. This is often the fastest way to confirm whether the issue is client-side routing/DNS or server-side reachability.

sudo wg show

If the handshake is present and counters increase when we generate traffic from Windows, the integration is successful.

Hardening and operational choices that matter in production

Restrict AllowedIPs to reduce blast radius

Full-tunnel (0.0.0.0/0) is convenient, but it is not always appropriate. In many enterprises, we only want VPN clients to reach specific internal networks. In that case, we narrow AllowedIPs on the client to only what is needed, for example:

  • AllowedIPs = 10.10.10.0/24 for VPN-only access
  • AllowedIPs = 10.10.10.0/24, 192.168.50.0/24 for a specific internal LAN behind the server

When we narrow AllowedIPs, we reduce accidental routing changes on endpoints and keep the VPN’s purpose crisp.

Keep keys and configs controlled

We should treat WireGuard private keys like SSH private keys. That means:

  • Strict file permissions (600 for configs, 700 for directories).
  • Unique keys per device, never shared across users.
  • Clear peer naming and IP assignment tracking.

Confirm persistence across reboots

We will verify that the server is enabled to start WireGuard on boot and that UFW is enabled.

sudo systemctl is-enabled wg-quick@wg0
sudo systemctl is-enabled ufw
sudo systemctl status wg-quick@wg0 --no-pager
sudo ufw status verbose

This confirms that both the VPN and firewall controls are persistent and active.

Troubleshooting: when it connects, but it doesn’t work

Symptom: No handshake on server (wg show shows “latest handshake: (none)”)

  • Likely cause: UDP/51820 is blocked by a firewall or missing port-forwarding.
  • Fix: Confirm the server is listening and the firewall allows UDP/51820, then confirm upstream NAT forwarding if applicable.
sudo ss -lunp | grep -E ':51820'
sudo ufw status verbose
sudo wg show

If the server is listening and UFW allows the port, we should check upstream security groups (cloud) or router port-forwarding (home/office).

Symptom: Handshake exists, but client cannot reach anything beyond the server

  • Likely cause: IP forwarding is disabled or NAT/forwarding rules are missing.
  • Fix: Confirm forwarding is enabled and that PostUp rules are applied when wg0 is up.
sysctl net.ipv4.ip_forward
sudo wg show
sudo ufw status verbose

If forwarding is 0, we must re-enable it and persist it. If forwarding is enabled, we should confirm that the NAT rule exists while wg0 is up.

sudo iptables -t nat -S POSTROUTING | grep -E '10.10.10.0/24'
sudo systemctl restart wg-quick@wg0
sudo iptables -t nat -S POSTROUTING | grep -E '10.10.10.0/24'

This confirms whether the MASQUERADE rule is being applied and re-applied correctly.

Symptom: Client connects, but DNS fails (IP works, names do not)

  • Likely cause: DNS servers are not reachable through the tunnel, or the client is not applying DNS settings.
  • Fix: Set DNS explicitly in the client config and verify name resolution while connected.
getent hosts example.com || true
resolvectl status 2>/dev/null | sed -n '1,120p' || true

If DNS is not applied, we should confirm the client config includes a DNS = line and that the platform’s resolver integration is installed (for example, resolvconf on Debian/Ubuntu clients).

Symptom: Client connects, but traffic routes incorrectly (some sites break, internal apps unreachable)

  • Likely cause: AllowedIPs is too broad or too narrow, causing route conflicts.
  • Fix: Decide whether we want full tunnel or split tunnel, then set AllowedIPs accordingly.
ip route | sed -n '1,120p'
sudo wg show

If we only need internal access, we should avoid 0.0.0.0/0 and list only the required subnets.

Common mistakes and clean fixes

Mistake: Reusing the same client key on multiple devices

Symptom: Devices randomly disconnect each other; handshakes “flip” between endpoints; traffic counters behave unpredictably.

Fix: Generate unique keys per device and create separate peer entries on the server with unique AllowedIPs (/32 per client).

Mistake: Assigning the same VPN IP to two peers

Symptom: One client works, the other fails; traffic goes to the wrong device; intermittent connectivity.

Fix: Ensure each peer has a unique AllowedIPs value on the server (for example, 10.10.10.2/32, 10.10.10.3/32, and so on).

Mistake: Forgetting to reload WireGuard after editing wg0.conf

Symptom: The config file looks correct, but the server behaves as if nothing changed.

Fix: Reload the running configuration safely.

sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
sudo wg show

This applies changes without tearing down the interface.

Mistake: Endpoint points to a private IP when the client is outside the network

Symptom: No handshake from outside networks; works only on the same LAN.

Fix: Use the server’s public IP or a public DNS name in Endpoint, and ensure UDP/51820 is forwarded to the server if behind NAT.

How do we at NIILAA look at this

This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.

At NIILAA, we help organizations design, deploy, secure, and maintain WireGuard-based remote access and site-to-site connectivity that fits real operational constraints: identity and access boundaries, key lifecycle, routing design, logging expectations, firewall policy, and change management. We focus on making the solution easy to run six months from now, not just easy to stand up today.

Website: https://www.niilaa.com
Email: [email protected]
LinkedIn: https://www.linkedin.com/company/niilaa
Facebook: https://www.facebook.com/niilaa.llc

Leave A Comment

All fields marked with an asterisk (*) are required