WireGuard marks a fundamental architecture shift in virtual private networks (VPN). Unlike traditional heavyweights such as OpenVPN or complex IPsec implementations, WireGuard runs directly as a lean Linux kernel module. With fewer than 4,000 lines of source code the protocol offers a minimal attack surface, modern cryptography (Curve25519, ChaCha20-Poly1305, BLAKE2s) and high throughput at minimal latency.
The essential difference from protocols such as OpenVPN or IPsec/IKEv2 is the protocol architecture: WireGuard does without complex userspace daemons and multi-stage certificate chains. Instead it runs a cryptographic 1-RTT handshake (1 round trip time) based on the proven Noise protocol framework (Noise_IKpsk2) directly in the Linux kernel. Sessions are negotiated silently and continue without a drop when the IP changes (for example from Wi-Fi to mobile) (roaming).
The following setup builds a production-ready WireGuard gateway on Ubuntu 24.04 LTS (Noble Numbat) and Ubuntu 26.04 LTS (Resolute Raccoon), including a cleanly split IPv4/IPv6 architecture, consistent UFW routing, pre-shared keys, Unbound DNS and hardened client management via QR code.
The network and crypto fundamentals taught here are an essential part of modern server environments and follow standards of exams such as the LPIC-1 series.
π‘ Note on kernel compatibility: WireGuard has been a fixed part of the official upstream kernel since Linux kernel 5.6. On Ubuntu 24.04 and Ubuntu 26.04 no external repositories (PPAs) or third-party kernel modules are required any longer.
WireGuard architecture and tunnel concept
In the WireGuard model every participant is an equal peer. In server practice the Ubuntu host acts as the central gateway that accepts client traffic encrypted and forwards it over the physical network interface into the local network or the internet:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WireGuard VPN architecture and tunnel β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Remote clients (roadwarrior) WireGuard gateway β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββ β
β β Laptop / smartphone β β Ubuntu Server β β
β β IP: 10.8.0.2 / GUA/ULA β β IP: 10.8.0.1 β β
β βββββββββββββββ¬ββββββββββββββ βββββββββββ¬ββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β Encrypted UDP tunnel (port 51820 / ChaCha20) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββ β
β β Internet and LAN β β
β β (routing / masq) β β
β βββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
IPv4 vs. IPv6 architecture: routed prefix vs. NAT66
Addressing and routing in the VPN tunnel differ fundamentally in the recommended architecture for IPv4 and IPv6:
IPv4 (RFC 1918 and NAT/masquerading):
Because public IPv4 addresses are scarce, VPN clients receive private addresses (for example from 10.8.0.0/24). The server masquerades outbound traffic through its public IP address using source NAT (masquerading).
IPv6 β preferred architecture (routed GUA prefix):
In IPv6, direct end-to-end reachability without NAT is the default. If your hosting provider gives the server a routed IPv6 subnet (for example its own /64 prefix from a /56 or /48 network), WireGuard clients receive real global unicast addresses (GUA). The server acts as a pure IPv6 router with no NAT at all.
IPv6 β alternative architecture (ULA with NAT66):
If the provider assigns the host only a single IPv6 address or a /64 network attached directly to the interface, without routing a separate subnet for clients, native routing without proxy NDP is not possible. In that case a unique local address prefix (ULA per RFC 4193, for example fd42:42:42::/64) combined with NAT66 (IPv6 masquerading) can be used. NAT66 is a deliberate transition solution and not a full equivalent of true IPv6 end-to-end connectivity.
The key facts of the setup:
| Parameter | Value / configuration | Purpose |
|---|---|---|
| VPN subnet (IPv4) | 10.8.0.0/24 |
Private RFC1918 address space for clients (server: 10.8.0.1) with NAT |
| VPN subnet (IPv6) | Routed GUA (preferred) or ULA fd42:42:42::/64 |
IPv6 client addresses (server: ...::1); ULA requires NAT66 |
| VPN port | 51820 / UDP |
Default port for inbound WireGuard tunnel traffic |
| Encryption | ChaCha20-Poly1305 (AEAD) | Authenticated symmetric encryption of data packets |
| Key exchange | Curve25519 (ECDH) + optional pre-shared key (PSK) | 1-RTT Noise IK handshake; PSK adds symmetric key material |
π‘ Note on post-quantum and pre-shared keys (PSK): WireGuard is not fully post-quantum safe by itself. The asymmetric key exchange is based on Curve25519, which future quantum computers could theoretically attack with Shor's algorithm ("harvest now, decrypt later"). An additional pre-shared key (
wg genpsk) mixes 256 bits of symmetric key material into the Noise protocol key derivation.That is a valuable extra line of defence against later decryption, but it does not replace a full post-quantum-safe asymmetric KEM (such as ML-KEM).
Preparation and installation
1. Update the system and install packages
Install the wireguard package (kernel module and control tools), qrencode for mobile QR codes and firewall tools:
sudo apt update && sudo apt upgrade -y
sudo apt install wireguard wireguard-tools qrencode iptables -y
π‘ iptables and nftables on Ubuntu: On Ubuntu 24.04 and 26.04 the system uses
iptables-nftby default. An abstraction layer translates classiciptablescommands directly into the modernnftableskernel subsystem. Tools such as UFW and existing scripts therefore continue to work reliably and performantly.
Check whether the WireGuard kernel module is loaded:
sudo modprobe wireguard
lsmod | grep wireguard
2. Enable IP forwarding in the Linux kernel
For the Linux host to forward packets between the VPN interface and the physical network interface, IP forwarding must be active for IPv4 and IPv6. Create a dedicated configuration file:
sudo tee /etc/sysctl.d/99-wireguard.conf << 'EOF'
# Enable IPv4 packet forwarding
net.ipv4.ip_forward = 1
# Enable IPv6 packet forwarding
net.ipv6.conf.all.forwarding = 1
EOF
Apply the parameters immediately:
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Verification:
Check that both parameters are actively set to 1:
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
Expected output:
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Key generation (server and peer)
WireGuard uses asymmetric cryptography: every participant has a private key (secret) and a derived public key. In addition we generate a symmetric pre-shared key (PSK) for every client.
Lock down the configuration directory with strict access rights (0700):
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
cd /etc/wireguard
Generate the key pair for the server with a safe file mask (umask 077):
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key
Read the public key (this is later entered in the client profiles):
echo "Server Public Key: $(cat server_public.key)"
Server configuration and firewall architecture
First determine the name of your primary network interface with internet access (for example eth0, enp1s0 or ens3):
ip -o -4 route show to default | awk '{print $5}'
In the rest of the text we use the interface eth0 as a stand-in. Adjust the identifier to the actual network interface of your server if needed.
To avoid security risks and rule collisions, host firewall and packet forwarding must be aligned cleanly. We distinguish two operating modes:
Variant A (recommended with UFW active):
UFW manages firewall, forwarding and NAT centrally. The file wg0.conf stays free of PostUp/PostDown netfilter commands.
Variant B (alternative without UFW):
Manages only the WireGuard-specific forwarding and NAT rules directly through PostUp and PostDown in wg-quick. Host INPUT rules (such as port 51820 UDP or port 53 for Unbound) must be maintained separately in the host firewall on restrictive systems.
π‘ Note on IPv6 in the examples: The following configuration examples use a ULA prefix (
fd42:42:42::/64) for IPv6 so the setup stays reproducible regardless of the hosting provider's prefix delegation. If you have a GUA prefix routed for the WireGuard network, replace the ULA addresses with addresses from your routed prefix and omit NAT66 from the firewall entirely.
Server configuration file (/etc/wireguard/wg0.conf)
Create the file /etc/wireguard/wg0.conf. If you use UFW (variant A), you do not need PostUp and PostDown lines:
SERVER_PRIV_KEY=$(cat /etc/wireguard/server_private.key)
sudo tee /etc/wireguard/wg0.conf << EOF
[Interface]
# Server tunnel IPs (dual-stack)
Address = 10.8.0.1/24, fd42:42:42::1/64
ListenPort = 51820
PrivateKey = ${SERVER_PRIV_KEY}
# With variant A (UFW), PostUp/PostDown are omitted here entirely.
# Peers are registered below.
EOF
Protect the file against unauthorised access:
sudo chmod 600 /etc/wireguard/wg0.conf
Variant A (recommended): consistent firewall and NAT with UFW
The blanket setting DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw is a common but problematic practice, because it disables filtering for every interface of the host.
A clean UFW architecture separates responsibilities clearly:
- UFW CLI (
ufw route): Controls filtering and forwarding targeted at interface and subnet level. /etc/ufw/before.rules: Manages IPv4 source NAT (masquerading), because UFW has no native CLI syntax for it.DEFAULT_FORWARD_POLICY="DROP": Stays unchanged in/etc/default/ufwto protect the host.
Step 1: Configure IPv4 NAT in /etc/ufw/before.rules
Open /etc/ufw/before.rules:
sudo nano /etc/ufw/before.rules
Insert the following nat block at the very start of the file (still before the first filter block):
# NAT rules for WireGuard VPN
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
If you use the ULA setup with NAT66 for IPv6, put in:
/etc/ufw/before6.rules
the analogous block before *filter:
*nat :POSTROUTING ... -A POSTROUTING -s fd42:42:42::/64 -o eth0 -j MASQUERADE COMMIT
With a routed GUA prefix this step is omitted entirely.
Step 2: Set up UFW routing rules and port openings
Configure forwarding directly through the dedicated UFW routing syntax:
# Allow the WireGuard VPN port for inbound tunnel traffic
sudo ufw allow 51820/udp comment 'WireGuard VPN Port'
# Allow local DNS access (Unbound) for WireGuard clients on the host (UDP and TCP)
sudo ufw allow in on wg0 to any port 53 proto udp comment 'DNS from WireGuard'
sudo ufw allow in on wg0 to any port 53 proto tcp comment 'DNS from WireGuard'
# Targeted forwarding: allow traffic from wg0 over the WAN interface (eth0)
sudo ufw route allow in on wg0 out on eth0 from 10.8.0.0/24
# For IPv6 (ULA or routed GUA):
sudo ufw route allow in on wg0 out on eth0 from fd42:42:42::/64
Return traffic for existing connections is handled automatically by UFW's default ruleset (-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT in before.rules).
Then reload UFW:
sudo ufw reload
Verification of UFW and netfilter rules:
# Shows status, port openings and UFW routing rules
sudo ufw status verbose
Note: ufw status lists only the filter and routing rules managed through the UFW CLI. The low-level netfilter rules defined directly in /etc/ufw/before.rules and /etc/ufw/before6.rules (such as masquerading) are not listed there. You can inspect the full raw table state with sudo ufw show raw or specifically through the kernel tables:
# Check IPv4 masquerading
sudo iptables -t nat -S POSTROUTING
# With a ULA setup and NAT66: check IPv6 masquerading
sudo ip6tables -t nat -S POSTROUTING
Variant B (alternative): netfilter management of tunnel rules through wg-quick
Variant B manages only the forwarding and NAT rules required for WireGuard through wg-quick. It does not replace a complete host firewall. With an existing restrictive INPUT policy, the WireGuard port UDP/51820 on the WAN interface and β when using Unbound β UDP/TCP 53 on wg0 must be opened separately in the host firewall.
To keep forwarding restrictive, outbound traffic is allowed only from wg0 to eth0, while inbound traffic on eth0 -> wg0 is accepted only for already existing connections (RELATED,ESTABLISHED):
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
For the ULA setup an analogous line with ip6tables for fd42:42:42::/64 can be added. With a routed GUA prefix the -t nat masquerading rule is omitted and only the FORWARD filter rules are needed.
Start and enable the WireGuard service
Start the wg0 interface through the systemd service wg-quick@wg0:
sudo systemctl enable --now wg-quick@wg0
Technical verification
Check the status of the core components systematically:
Check service status:
sudo systemctl status wg-quick@wg0 --no-pager
Verify network interface and IP addresses:
ip addr show wg0
Expectation: the interface exists, has status UP and carries the addresses 10.8.0.1/24 and fd42:42:42::1/64.
Check the UDP socket:
sudo ss -ulnp | grep 51820
Expectation: the kernel is listening on 0.0.0.0:51820 and [::]:51820.
Query WireGuard kernel status:
sudo wg show
Expected output:
interface: wg0
public key: <SERVER_PUBLIC_KEY>
private key: (hidden)
listening port: 51820
Create and connect a client (peer)
We now configure the first client as an example (for example a smartphone or notebook).
1. Generate client keys and pre-shared key
Create a protected directory for client profiles:
sudo mkdir -p /etc/wireguard/clients
sudo chmod 700 /etc/wireguard/clients
cd /etc/wireguard/clients
umask 077
# Generate key pair and symmetric PSK
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
wg genpsk > client1_preshared.key
2. Register the peer on the server
Add the new peer to /etc/wireguard/wg0.conf:
CLIENT1_PUB=$(cat client1_public.key)
CLIENT1_PSK=$(cat client1_preshared.key)
sudo tee -a /etc/wireguard/wg0.conf << EOF
# Client 1: smartphone / laptop
[Peer]
PublicKey = ${CLIENT1_PUB}
PresharedKey = ${CLIENT1_PSK}
AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128
EOF
3. Reload the configuration without dropping connections (wg syncconf)
To apply new peers, do not use systemctl restart, but the command wg syncconf:
sudo wg syncconf wg0 <(wg-quick strip wg0)
Why wg syncconf?
- The helper
wg-quick strip wg0filters out helper directives (such asAddress,DNSorPostUp) so a pure WireGuard ruleset remains for the kernel commandwg. wg syncconfcompares the desired configuration with the WireGuard configuration currently active in the kernel and applies only the necessary changes (delta).- Existing peer sessions and connections are therefore not disturbed unnecessarily.
- Pure peer changes do not require a full interface restart, and transfer statistics plus handshake timers stay intact.
4. Create the client configuration file
Enter the reachable server endpoint (public IPv4 address or FQDN):
SERVER_ENDPOINT="vpn.example.com" # Or the public IP of the server
SERVER_PUB=$(cat /etc/wireguard/server_public.key)
CLIENT1_PRIV=$(cat client1_private.key)
tee /etc/wireguard/clients/client1.conf << EOF
[Interface]
PrivateKey = ${CLIENT1_PRIV}
Address = 10.8.0.2/24, fd42:42:42::2/64
# DNS resolver of the gateway (Unbound in step 7) or external resolvers first (e.g. 1.1.1.1)
DNS = 10.8.0.1, fd42:42:42::1
[Peer]
PublicKey = ${SERVER_PUB}
PresharedKey = ${CLIENT1_PSK}
Endpoint = ${SERVER_ENDPOINT}:51820
# 0.0.0.0/0, ::/0 sends all traffic through the VPN (full tunnel)
AllowedIPs = 0.0.0.0/0, ::/0
# Keeps the connection stable behind stateful firewalls / NAT routers
PersistentKeepalive = 25
EOF
Full tunnel vs. split tunneling:
By default AllowedIPs = 0.0.0.0/0, ::/0 sends all client traffic through the VPN (full tunnel). If instead only access to the VPN subnet or internal company networks should go through the tunnel, while regular internet browsing stays on the client's local line (split tunneling), adjust the directive in the client profile accordingly.
Keep consistency with the configured DNS server: if besides the IPv4 subnets the IPv6 resolver (fd42:42:42::1) or internal IPv6 services should be reachable through the tunnel, the matching IPv6 VPN prefix must also be listed in AllowedIPs:
# Dual-stack split tunnel (including IPv6 DNS resolver)
AllowedIPs = 10.8.0.0/24, fd42:42:42::/64, 192.168.10.0/24
If instead a pure IPv4 split tunnel (AllowedIPs = 10.8.0.0/24, 192.168.10.0/24) is set up, the client profile under DNS = may list only the IPv4 address of the resolver (DNS = 10.8.0.1), because queries to fd42:42:42::1 would otherwise be dropped by the client for lack of a routing entry.
π‘ DNS entry vs. transport encryption: The
DNS = ...parameter in the client configuration only assigns the operating system the IP address of the resolver to use. It does not enable DNS-over-TLS (DoT) or DNS-over-HTTPS (DoH). In a full tunnel, DNS packets are encrypted to the server inside the VPN tunnel, but leave it toward the upstream resolver unencrypted over port 53 by default (unless a local DoT resolver such as Unbound is running on the server).
5. Generate a QR code for smartphones
For mobile devices with the official WireGuard app, the configuration can be shown directly in the terminal as an ANSI QR code:
qrencode -t ansiutf8 < /etc/wireguard/clients/client1.conf
Open the app on the smartphone, choose + β Scan from QR code, point the camera at the terminal and activate the tunnel.
Automation: hardened client-management script
To create new clients safely and reproducibly with a one-liner, a hardened management script is recommended. The following script avoids external web lookups, validates input strictly, prevents address collisions, converts IPv6 suffixes to hexadecimal correctly and works fault-tolerantly through temporary files, file locking and a controlled rollback mechanism.
Create the file /usr/local/bin/add-wireguard-client:
sudo tee /usr/local/bin/add-wireguard-client << 'EOF'
#!/bin/bash
set -euo pipefail
# ==============================================================================
# Configuration
# ==============================================================================
WG_DIR="/etc/wireguard"
WG_CONF="$WG_DIR/wg0.conf"
CLIENT_DIR="$WG_DIR/clients"
LOCK_FILE="/var/lock/wireguard-client.lock"
# Enter the fixed FQDN or public IP of your server here
SERVER_ENDPOINT="vpn.example.com"
SERVER_PORT=51820
SERVER_PUB_KEY_FILE="$WG_DIR/server_public.key"
# Address spaces
VPN_NET_PREFIX="10.8.0"
VPN_IPV6_PREFIX="fd42:42:42::"
DNS_SERVERS="10.8.0.1, fd42:42:42::1"
# ==============================================================================
# Validation and concurrency locking
# ==============================================================================
if [ "$EUID" -ne 0 ]; then
echo "Error: this script must be run with administrative rights (sudo)." >&2
exit 1
fi
if [ -z "${1:-}" ]; then
echo "Usage: sudo $0 <client-name> [ip-suffix]" >&2
echo "Example: sudo $0 notebook 3" >&2
exit 1
fi
CLIENT_NAME="$1"
IP_SUFFIX="${2:-}"
# Validate client names (2-32 characters, alphanumeric plus '-' and '_')
if ! [[ "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]{2,32}$ ]]; then
echo "Error: invalid client name '$CLIENT_NAME'. Allowed are 2-32 characters [a-zA-Z0-9_-]." >&2
exit 1
fi
# Concurrency locking via flock against race conditions
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
echo "Error: another instance of this script is already running." >&2
exit 1
fi
if [ ! -f "$WG_CONF" ] || [ ! -f "$SERVER_PUB_KEY_FILE" ]; then
echo "Error: $WG_CONF or $SERVER_PUB_KEY_FILE does not exist." >&2
exit 1
fi
CONF_FILE="$CLIENT_DIR/${CLIENT_NAME}.conf"
if [ -f "$CONF_FILE" ]; then
echo "Error: configuration file '$CONF_FILE' already exists." >&2
exit 1
fi
if grep -qE "^\s*#\s*Peer:\s*${CLIENT_NAME}\s*$" "$WG_CONF"; then
echo "Error: a peer named '$CLIENT_NAME' is already present in $WG_CONF." >&2
exit 1
fi
# ==============================================================================
# IP assignment and collision check (IPv4 decimal, IPv6 hexadecimal)
# ==============================================================================
if [ -n "$IP_SUFFIX" ]; then
if ! [[ "$IP_SUFFIX" =~ ^[0-9]+$ ]] || [ "$IP_SUFFIX" -lt 2 ] || [ "$IP_SUFFIX" -gt 254 ]; then
echo "Error: the IP suffix must be an integer between 2 and 254." >&2
exit 1
fi
printf -v IPV6_HEX '%x' "$IP_SUFFIX"
CANDIDATE_IPV4="${VPN_NET_PREFIX}.${IP_SUFFIX}"
CANDIDATE_IPV6="${VPN_IPV6_PREFIX}${IPV6_HEX}"
if grep -qF "$CANDIDATE_IPV4" "$WG_CONF" || grep -qF "$CANDIDATE_IPV6" "$WG_CONF"; then
echo "Error: IP address ($CANDIDATE_IPV4 or $CANDIDATE_IPV6) is already assigned in $WG_CONF." >&2
exit 1
fi
else
# Automatic assignment of the next free IP
IP_SUFFIX=0
for i in $(seq 2 254); do
TEST_IPV4="${VPN_NET_PREFIX}.${i}"
printf -v TEST_HEX '%x' "$i"
TEST_IPV6="${VPN_IPV6_PREFIX}${TEST_HEX}"
if ! grep -qF "$TEST_IPV4" "$WG_CONF" && ! grep -qF "$TEST_IPV6" "$WG_CONF"; then
IP_SUFFIX="$i"
break
fi
done
if [ "$IP_SUFFIX" -eq 0 ]; then
echo "Error: no free addresses in the range ${VPN_NET_PREFIX}.2-254." >&2
exit 1
fi
fi
CLIENT_IPV4="${VPN_NET_PREFIX}.${IP_SUFFIX}"
printf -v IPV6_HEX '%x' "$IP_SUFFIX"
CLIENT_IPV6="${VPN_IPV6_PREFIX}${IPV6_HEX}"
# ==============================================================================
# Safe creation via temporary files with rollback
# ==============================================================================
mkdir -p "$CLIENT_DIR"
umask 077
TMP_CLIENT_CONF=$(mktemp "$CLIENT_DIR/.client.XXXXXX")
TMP_WG_CONF=$(mktemp --suffix=.conf "$WG_DIR/wgtmp.XXXXXX")
chmod 600 "$TMP_CLIENT_CONF" "$TMP_WG_CONF"
cleanup() {
rm -f "$TMP_CLIENT_CONF" "$TMP_WG_CONF" "${WG_CONF}.bak"
}
trap cleanup EXIT INT TERM
echo "==> Generating key material for peer: $CLIENT_NAME ($CLIENT_IPV4 / $CLIENT_IPV6)..."
PRIV_KEY=$(wg genkey)
PUB_KEY=$(echo "$PRIV_KEY" | wg pubkey)
PSK=$(wg genpsk)
SERVER_PUB=$(cat "$SERVER_PUB_KEY_FILE")
# Write the client profile temporarily
cat > "$TMP_CLIENT_CONF" << CLIENT_CONF
[Interface]
PrivateKey = ${PRIV_KEY}
Address = ${CLIENT_IPV4}/24, ${CLIENT_IPV6}/64
DNS = ${DNS_SERVERS}
[Peer]
PublicKey = ${SERVER_PUB}
PresharedKey = ${PSK}
Endpoint = ${SERVER_ENDPOINT}:${SERVER_PORT}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
CLIENT_CONF
# Prepare the new server configuration
cp -p "$WG_CONF" "$TMP_WG_CONF"
cat >> "$TMP_WG_CONF" << PEER_CONF
# Peer: ${CLIENT_NAME}
[Peer]
PublicKey = ${PUB_KEY}
PresharedKey = ${PSK}
AllowedIPs = ${CLIENT_IPV4}/32, ${CLIENT_IPV6}/128
PEER_CONF
# Syntax check of the new configuration before activation
if ! wg-quick strip "$TMP_WG_CONF" > /dev/null 2>&1; then
echo "Error: the generated server configuration is syntactically invalid. Aborting." >&2
exit 1
fi
# Update the server configuration with a backup
cp -p "$WG_CONF" "${WG_CONF}.bak"
cp "$TMP_WG_CONF" "$WG_CONF"
# Live sync with the kernel
if ! wg syncconf wg0 <(wg-quick strip "$WG_CONF"); then
echo "Error: kernel sync via wg syncconf failed. Rolling back..." >&2
cp -p "${WG_CONF}.bak" "$WG_CONF"
if ! wg syncconf wg0 <(wg-quick strip "$WG_CONF"); then
echo "CRITICAL: rollback of the running WireGuard configuration failed." >&2
fi
exit 1
fi
# Move the client profile atomically into place
mv "$TMP_CLIENT_CONF" "$CONF_FILE"
rm -f "${WG_CONF}.bak"
echo "==> Profile saved successfully: $CONF_FILE"
echo ""
echo "=== QR CODE FOR MOBILE DEVICES ==="
qrencode -t ansiutf8 < "$CONF_FILE"
EOF
sudo chmod +x /usr/local/bin/add-wireguard-client
New clients can now be created safely:
sudo add-wireguard-client tablet
sudo add-wireguard-client macbook 5
Site-to-site VPN: linking two networks
While the roadwarrior setup attaches individual endpoints, a site-to-site VPN couples two complete networks transparently. Devices at site A (192.168.10.0/24) can reach servers at site B (192.168.20.0/24) directly without client software:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Site-to-site VPN: linking locations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Site A (office / cloud) Site B (home lab) β
β LAN: 192.168.10.0/24 LAN: 192.168.20.0/24 β
β βββββββββββββββββββββ βββββββββββββββββββββ β
β β Gateway A (Ubuntu)β β Gateway B (Ubuntu)β β
β β IP: 10.8.0.1 β β IP: 10.8.0.2 β β
β βββββββββββ¬ββββββββββ βββββββββββ¬ββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β Persistent WireGuard tunnel (UDP port 51820) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Transparent routing (no NAT): β
β - Gateway A routes 192.168.20.0/24 -> 10.8.0.2 β
β - Gateway B routes 192.168.10.0/24 -> 10.8.0.1 β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Important prerequisites for site-to-site
No overlapping IP subnets:
The two sites must use different IP ranges (for example 192.168.10.0/24 and 192.168.20.0/24). If both sides use the same address range (such as the common 192.168.1.0/24), direct routing is impossible.
IP forwarding on both gateways:
On gateway A and gateway B, net.ipv4.ip_forward = 1 must be active.
No NAT between the sites: Tunnel traffic between the networks typically uses no masquerading, so source IPs stay unchanged for monitoring, logging and firewall rules.
Return routes on the LAN router: Clients at site A send packets to their default gateway (for example FRITZ!Box or pfSense). The main router must know how the remote network is reached. Therefore a static route must be set on the main router at site A:
Destination network:
192.168.20.0/24
Gateway / next hop:
- IP address of gateway A on the local network (for example
192.168.10.254). - Analogously at site B:
192.168.10.0/24via192.168.20.254.
1. Configuration of gateway A (/etc/wireguard/wg0.conf)
Gateway A acts as responder with a publicly reachable address or DynDNS:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <GATEWAY_A_PRIVATE_KEY>
# Allow forwarding between local LAN (eth0) and VPN (wg0) (no NAT)
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -j ACCEPT
# Peer: Gateway B
[Peer]
PublicKey = <GATEWAY_B_PUBLIC_KEY>
PresharedKey = <SITE_TO_SITE_PSK>
# Allow both the tunnel IP and the entire remote LAN
AllowedIPs = 10.8.0.2/32, 192.168.20.0/24
2. Configuration of gateway B (/etc/wireguard/wg0.conf)
Gateway B actively builds the connection to gateway A:
[Interface]
Address = 10.8.0.2/24
PrivateKey = <GATEWAY_B_PRIVATE_KEY>
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -j ACCEPT
# Peer: Gateway A
[Peer]
PublicKey = <GATEWAY_A_PUBLIC_KEY>
PresharedKey = <SITE_TO_SITE_PSK>
Endpoint = gateway-a.example.com:51820
AllowedIPs = 10.8.0.1/32, 192.168.10.0/24
PersistentKeepalive = 25
3. Bidirectional verification
Check the connection in both directions:
# 1. Check the tunnel handshake (on both gateways)
sudo wg show
# 2. Ping the remote tunnel IP (from gateway A)
ping -c 3 10.8.0.2
# 3. Ping a host on the remote LAN (from gateway A)
ping -c 3 192.168.20.50
# 4. Traceroute from a regular client on LAN A
traceroute 192.168.20.50
With unfiltered traceroute/ICMP traffic, the local WireGuard gateway and then the remote gateway should typically appear first. Missing intermediate hops (for example asterisks *) do not automatically mean a routing error; they often appear when firewalls or routers between the sites drop ICMP time-exceeded messages.
Own DNS server and internal name resolution (Unbound)
If VPN clients should resolve internal hostnames (for example nas.intern or wiki.corp) without forwarding DNS queries unencrypted to public resolvers, a local Unbound DNS resolver on the server is a good fit.
Install Unbound:
sudo apt install unbound -y
Unbound configuration with DNS-over-TLS (DoT)
Here we cleanly separate local name resolution from the type of forwarding:
- Variant 1 (recommended): forwarding over DNS-over-TLS (DoT): Encrypts external DNS queries between your server and upstream resolvers (for example Quad9) over port 853 with certificate validation.
- Variant 2: standard forwarding: Forwards queries unencrypted over the traditional UDP/TCP port 53.
Create the configuration file /etc/unbound/unbound.conf.d/wireguard.conf:
sudo tee /etc/unbound/unbound.conf.d/wireguard.conf << 'EOF'
server:
interface: 10.8.0.1
interface: fd42:42:42::1
interface: 127.0.0.1
interface: ::1
access-control: 10.8.0.0/24 allow
access-control: fd42:42:42::/64 allow
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
# Security and hardening settings
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
# Define a local infrastructure zone
local-zone: "intern." static
local-data: "nas.intern. IN A 192.168.10.50"
local-data: "wiki.intern. IN A 192.168.10.51"
local-data: "router.intern. IN A 10.8.0.1"
# Variant 1: encrypted upstream forwarding via DNS-over-TLS (port 853)
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
# Variant 2 (alternative for standard DNS without TLS on port 53):
# forward-zone:
# name: "."
# forward-tls-upstream: no
# forward-addr: 9.9.9.9
# forward-addr: 149.112.112.112
EOF
Start Unbound and check the service:
sudo systemctl enable --now unbound
sudo systemctl status unbound --no-pager
In the client profiles under [Interface], now enter DNS = 10.8.0.1, fd42:42:42::1, intern. The client sends all DNS queries through the VPN tunnel to Unbound. When using a routed GUA prefix, replace the ULA addresses with your GUA server addresses. Make sure the host firewall (UFW or nftables) allows queries on port 53 over the wg0 interface (as set up in the firewall sections), because DNS traffic to the gateway is filtered as local host traffic (INPUT).
π‘ DNS integration of
wg-quickon Linux:wg-quickprocesses theDNS =directive through theresolvconfcommand. On Ubuntu this interface can be provided by the resolvconf compatibility ofsystemd-resolved. If a WireGuard profile is instead managed or imported through NetworkManager, NetworkManager takes over DNS integration directly.After the connection is up, check on the client with
resolvectl status wg0whether the assigned resolvers and β depending on the resolver integration in use β the configured search/routing domain forinternare in effect.
Strict VPN kill switch for mobile clients
On mobile laptops (for example on public networks or in hotels) a VPN drop must not let unencrypted traffic flow over the local interface.
β οΈ Before enabling the kill switch: The command
sudo ufw default deny outgoingimmediately blocks all outbound network traffic of the machine. If you run this step carelessly over an existing SSH connection, or if required exceptions for DHCP and LAN are missing, you cut yourself off from the system immediately. Set the exceptions exactly in the given order.
Architecture and a deliberate design choice: IPv4 bootstrap and strict tunnel
A mobile Linux client (laptop) often moves between changing guest Wi-Fi, hotels or mobile hotspots. A fully universal dual-stack kill switch for arbitrary foreign networks is complex at operating-system level, because local IPv6 prefixes, router advertisements and DNS behaviour change dynamically.
We therefore use a deliberate, hardened reference architecture:
Fixed IPv4 server address for the handshake:
The client builds the WireGuard tunnel specifically over the public IPv4 address of the server (Endpoint = <SERVER_IPV4>:51820). That removes any unencrypted DNS bootstrap communication to the outside before the tunnel is up.
Minimal IPv4 bootstrap over the physical interface:
For connection setup, loopback, DHCPv4 (UDP ports 67/68) and the WireGuard handshake (UDP port 51820) are opened on the physical interface. Besides loopback, DHCPv4 and the WireGuard handshake, access to the immediately attached local network can optionally be allowed. That exception means local LAN traffic deliberately does not go through the VPN tunnel; on foreign hotel or guest Wi-Fi this step should be omitted for maximum isolation.
All payload traffic (IPv4 and IPv6) exclusively over wg0:
Once the tunnel is up, all regular traffic β IPv4 as well as IPv6 β flows encrypted through the wg0 interface.
IPv6 payload outside wg0 blocked:
Regular outbound IPv6 payload over the physical interface is fully blocked by default deny outgoing (no IPv6 leak past the tunnel).
Local link communication:
The ICMPv6 packets required for basic local interface initialisation (neighbor discovery, router advertisements) are processed automatically in UFW's system-wide default rules (/etc/ufw/before6.rules) at link-local level (fe80::/10), so the physical interface stays stable on the local network without letting internet traffic through.
UFW rules for the kill switch (on the Linux client)
Apply the configuration on the client step by step:
# 1. Fully allow the local loopback interface
sudo ufw allow out on lo to any
# 2. Allow DHCPv4 for local IP negotiation on the physical interface
sudo ufw allow out 67:68/udp comment 'DHCP Client'
# 3. Optional: allow the local subnet (only if needed, e.g. home LAN; omit on foreign nets)
sudo ufw allow out to 192.168.1.0/24 comment 'Local LAN (optional)'
# 4. Allow connection setup to the WireGuard server over IPv4 (fixed server IP!)
sudo ufw allow out to <SERVER_IPV4> proto udp port 51820 comment 'WireGuard Handshake'
# 5. Allow all payload traffic (IPv4 and IPv6) over the tunnel interface
sudo ufw allow out on wg0 to any
# 6. Block inbound and outbound traffic on all interfaces by default
sudo ufw default deny incoming
sudo ufw default deny outgoing
# 7. Enable the firewall
sudo ufw enable
Disable in an emergency:
If you want to lift the kill switch again:
sudo ufw default allow outgoing
sudo ufw reload
Monitoring and troubleshooting
Successful troubleshooting strictly separates three layers: cryptographic handshake, IP routing and DNS resolution.
1. Handshake check (wg show)
Run the following command on the server or client:
sudo wg show
Sample output:
interface: wg0
public key: 4HkL...8Xg=
private key: (hidden)
listening port: 51820
peer: 4HkL...8Xg=
preshared key: (hidden)
endpoint: 203.0.113.42:54120
allowed ips: 10.8.0.2/32, fd42:42:42::2/128
latest handshake: 14 seconds ago
transfer: 2.14 MiB received, 18.76 MiB sent
β οΈ Handshake missing on connection attempts? WireGuard generates handshakes on demand: without outbound or inbound traffic there is no rekeying; an older
latest handshakein idle state is therefore completely normal. If, however, actively generated traffic (for example a running ping) produces no current handshake, orlatest handshakedoes not update despite repeated connection attempts, the usual causes are:
- UDP port
51820is blocked in a firewall (for example a cloud security group or router).- The server endpoint (domain or IPv4) is not reachable for the client (for example DS-Lite / CGNAT).
- PublicKey or PresharedKey do not match exactly between server and client.
- On clients behind NAT, a missing
PersistentKeepalive = 25can make a previously working connection sleep after long idle periods and no longer be immediately reachable from the outside.
2. Routing check
If there is a current handshake, test routing:
# Ping the gateway
ping -c 3 10.8.0.1
# Ping an external IP (checks forwarding and masquerading)
ping -c 3 1.1.1.1
# Query the routing table for a destination IP
ip route get 1.1.1.1
If 10.8.0.1 is reached but 1.1.1.1 is not, kernel forwarding (sysctl net.ipv4.ip_forward) or the UFW/NAT rule on the server is faulty.
3. Understand MTU and path MTU discovery
wg-quick determines the WireGuard tunnel MTU automatically by default from the routing to the endpoint or the MTU of the underlying interface.
A tunnel MTU of 1420 bytes is common on classic setups with a standard 1500-byte Ethernet underlay, but it is not a universal WireGuard value:
Divergent underlays:
Links with PPPoE (typically MTU 1492), extra encapsulations (DS-Lite, GRE, VXLAN), mobile (LTE/5G) or nested VPNs reduce the actual path MTU (PMTU) of the connection.
Manual MTU configuration:
A manual MTU = setting in the client or server configuration should only be used if automatic detection fails or concrete PMTU problems have been diagnosed (for example stalls on large websites or SSH transfers).
PMTU diagnosis with ping:
With the ping command and the don't-fragment flag set (-M do) you can test up to which size unfragmented IP packets can be transported over the path. Note what is being measured: on IPv4 the total IP packet size is the ICMP payload plus 28 bytes of headers (20 bytes IPv4 header + 8 bytes ICMP header):
$$\text{IP packet size} = \text{payload} + 28\text{ bytes}$$
# Tests an IP packet of 1420 bytes (1392 bytes payload + 28 bytes header)
ping -M do -s 1392 -c 3 <SERVER_IP>
If this test is rejected with Frag needed and DF set, reduce the payload in steps of 8 (for example to 1364 or 1332) to find the maximum unfragmented packet size on this path.
β Important note on MTU discovery: The measurement reflects the MTU of the transport path and cannot be taken 1:1 as a universal WireGuard MTU. On transfer problems it does, however, give the decisive hint for a targeted adjustment of the
MTU =value in the client profile under[Interface].
DNS leak test and encryption
When the VPN tunnel is active, all DNS queries must be sent through the VPN. Otherwise DNS queries flow past the tunnel unencrypted to the local internet service provider (DNS leak).
Test for DNS leaks
Step 1: Establish the tunnel connection
Activate the WireGuard tunnel on the client:
sudo wg-quick up wg0
Step 2: Check the local system resolver
Verify in the terminal which DNS servers and routing domains the system resolver actually uses for the tunnel interface:
# Check status and assigned resolvers of the tunnel interface
resolvectl status wg0
# Run a test query through the configured resolver
resolvectl query example.com
Step 3: Run external leak detection
Test externally through which resolver infrastructure your queries arrive on the web (without forcing a resolver with @):
dig +short TXT whoami.ds.akahelp.net
Alternatively open test pages such as https://dnsleaktest.com or https://ipleak.net in the browser.
Step 4: Assess the test result
A DNS leak is not defined by resolver ownership alone, but by DNS queries using an unexpected resolver path outside the intended VPN/DNS architecture. With the configuration described here, DNS queries should not go through the resolver of the local internet provider (ISP). If that resolver is still used although Unbound or the configured VPN resolver is intended, that points to a DNS leak or faulty resolver prioritisation on the client.
If public anycast resolvers (such as Quad9 or Cloudflare) are used, they do not have to answer geographically at the same site as your VPN gateway, but they must not be queried unencrypted over the physical connection of the local internet access.
β οΈ Tip on DNS leaks: If local DNS servers of the home router are shown, check whether
systemd-resolvedor the local connection manager has correctly prioritised the WireGuard interface.
nftables alternative for advanced operators
For administrators who want to maintain a purely native nftables ruleset instead of UFW, nftables offers atomic updates and a common syntax for IPv4 and IPv6.
Complete host firewall with WireGuard (/etc/nftables.d/wireguard.nft)
The following ruleset acts as a complete, restrictive host firewall (policy drop). It allows loopback, existing connections, network diagnostics via ICMP/ICMPv6, SSH administration and WireGuard tunnel traffic. If the administration architecture allows it, SSH should additionally be restricted to known management networks or trusted source addresses:
sudo mkdir -p /etc/nftables.d
sudo tee /etc/nftables.d/wireguard.nft << 'EOF'
#!/usr/sbin/nft -f
# Complete host firewall including WireGuard VPN and NAT
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Allow the local loopback interface
iif "lo" accept comment "Allow loopback"
# Allow existing and related connections
ct state established,related accept comment "Allow established/related"
# Drop invalid packets
ct state invalid drop comment "Drop invalid packets"
# Allow ICMP and ICMPv6 for network diagnostics and path MTU discovery
ip protocol icmp accept comment "Allow IPv4 ICMP (ping, PMTU)"
ip6 nexthdr icmpv6 accept comment "Allow IPv6 ICMPv6 (NDP, ping, PMTU)"
# SSH access for administration (adjust the port if different)
tcp dport 22 accept comment "Allow SSH management"
# Allow inbound WireGuard VPN port
udp dport 51820 accept comment "Allow WireGuard VPN port"
# Allow local DNS access (Unbound) for WireGuard clients (UDP and TCP)
iifname "wg0" udp dport 53 accept comment "DNS from WireGuard"
iifname "wg0" tcp dport 53 accept comment "DNS from WireGuard"
}
chain forward {
type filter hook forward priority filter; policy drop;
# Allow existing connections in forwarding
ct state established,related accept comment "Forward established/related"
# Forward traffic from the VPN to the internet (eth0)
iifname "wg0" oifname "eth0" accept comment "WireGuard client forwarding"
}
}
table inet nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# IPv4 masquerading (NAT) for the WireGuard subnet over WAN
ip saddr 10.8.0.0/24 oifname "eth0" masquerade comment "WireGuard IPv4 NAT"
# Optional IPv6 NAT66 (only required for ULA configuration)
# ip6 saddr fd42:42:42::/64 oifname "eth0" masquerade comment "WireGuard IPv6 NAT66"
}
}
EOF
sudo chmod 600 /etc/nftables.d/wireguard.nft
Include in /etc/nftables.conf
So that the ruleset is loaded at system start, include the directory in /etc/nftables.conf:
if ! grep -q 'include "/etc/nftables.d/\*.nft"' /etc/nftables.conf; then
echo 'include "/etc/nftables.d/*.nft"' | sudo tee -a /etc/nftables.conf
fi
# Check and load the configuration
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftables
Verification:
sudo nft list ruleset
Firewall sets for granular access control (ACLs)
Sets in nftables are useful when you do not want to give every peer the same rights, but maintain access control lists (ACLs) (for example only certain admin peers may reach internal SSH services):
# Define a set for privileged peers in the 'filter' table
sudo nft add set inet filter admin_peers '{ type ipv4_addr; }'
sudo nft add element inet filter admin_peers '{ 10.8.0.2, 10.8.0.5 }'
# Allow only members of the set access to SSH on the internal management network
sudo nft add rule inet filter forward ip saddr @admin_peers ip daddr 192.168.10.10 tcp dport 22 accept
Logging and fault diagnosis
1. Temporarily enable dynamic kernel debugging
WireGuard is silent in normal operation and produces no kernel-log messages by default. On difficult connection drops you can enable kernel debugging:
# Turn on debug logging for the kernel module
echo "module wireguard +p" | sudo tee /sys/kernel/debug/dynamic_debug/control
# Follow messages live in the system log
sudo dmesg -wT | grep -i wireguard
After the analysis you should disable logging again to avoid log overhead:
echo "module wireguard -p" | sudo tee /sys/kernel/debug/dynamic_debug/control
2. Analyse systemd logs and interfaces
# Read systemd events from wg-quick
journalctl -u wg-quick@wg0 --since "1 hour ago"
# Machine-readable summary of all peers
sudo wg show wg0 dump
# Check socket status and queues
sudo ss -ulnp 'sport = :51820'
Command Reference (Cheatsheet)
| Command | Category | Purpose and operational effect |
|---|---|---|
sudo systemctl start wg-quick@wg0 |
Service control | Starts the wg0 interface and sets up tunnel routes |
sudo systemctl stop wg-quick@wg0 |
Service control | Stops the interface and tears down tunnel routes |
sudo systemctl enable --now wg-quick@wg0 |
Autostart | Enables automatic system start of the VPN tunnel |
sudo wg show |
Monitoring | Shows active peers, handshake times, endpoints and transfer volume |
sudo wg show wg0 dump |
Scripting / API | Outputs tabular, machine-readable peer and tunnel data |
sudo wg syncconf wg0 <(wg-quick strip wg0) |
Live reload | Syncs peer changes in the running kernel without dropping active tunnels |
wg genkey | tee priv.key | wg pubkey > pub.key |
Cryptography | Generates an asymmetric Curve25519 key pair |
wg genpsk > preshared.key |
Cryptography | Generates a symmetric 256-bit pre-shared key (extra protection) |
qrencode -t ansiutf8 < client.conf |
Deployment | Renders a configuration file as a scannable terminal QR code |
sudo add-wireguard-client <name> [suffix] |
Automation | Creates a new profile fully automatically, fault-tolerant with file locking and rollback |
sudo ufw route allow in on wg0 out on eth0 |
Firewall | Allows targeted forwarding of WireGuard clients over the WAN interface |
sudo ufw status verbose |
Firewall | Shows UFW status and port openings in detail |
sudo nft -f /etc/nftables.conf |
Firewall | Loads the native nftables ruleset atomically |
echo "module wireguard +p" | sudo tee ... |
Troubleshooting | Enables dynamic kernel debugging for detailed diagnosis |
resolvectl status wg0 |
DNS | Shows DNS resolver assignment and DNSSEC on the interface |
Further Resources
| Resource | Link | Purpose and content |
|---|---|---|
| WireGuard official | WireGuard Technical Whitepaper | Official protocol design, Noise framework and crypto analysis |
| WireGuard tools and quick | WireGuard manpages | Official reference for wg and wg-quick parameters |
| Ubuntu Server Guide | Ubuntu Server Documentation | Reference for network configuration, kernel routing and UFW |
| Linux server hardening | Linux server hardening: SSH and CrowdSec | Security guidelines, SSH with FIDO2 and firewall hardening |
| Ubuntu upgrade | Ubuntu 24.04 to 26.04 LTS upgrade | System upgrade of kernel and package sources to the next LTS version |
| LPIC-1 course series | LPIC-1 series: Linux administration | Solid course fundamentals on subnets, routing and permissions |
Conclusion
WireGuard provides a modern, high-performance VPN architecture for Linux servers: lean in code, implemented directly as a kernel module and cryptographically precise on modern primitives (Curve25519, ChaCha20-Poly1305, BLAKE2s). Direct support in current Ubuntu LTS releases gives a stable, low-maintenance base for gateways and site couplings.
The main operational takeaways at a glance:
Protocol design and handshake:
WireGuard does without heavy userspace connection daemons. The 1-RTT handshake is based on the Noise protocol framework and is handled directly in the kernel, which speeds connection setup and enables seamless roaming.
Cryptographic classification: Symmetric pre-shared keys (PresharedKey) give valuable protection against retroactive decryption ("harvest now, decrypt later"), but they do not make the setup a complete post-quantum-safe KEM.
Architecture for IPv4 and IPv6: IPv4 works by default with a private subnet and NAT. For IPv6 a routed GUA prefix is the preferred standard; ULA with NAT66 is a practical alternative when the provider does not route a client prefix.
Consistent firewall control: Avoid uncontrolled global openings (DEFAULT_FORWARD_POLICY="ACCEPT"). Maintain forwarding and NAT in a structured way in the UFW rule files or through a standalone nftables ruleset.
Uninterrupted operation: Updating peers through wg syncconf protects active VPN sessions from drops and enables clean automation in production.
On new setups start with a single client and verify handshake (wg show), IP routing (ping) and DNS resolution (resolvectl status) systematically as separate layers before rolling out further peers.