$linuxjunkies
>

Set Up a WireGuard VPN Server

Configure a WireGuard VPN server from scratch: key generation, server and client configs, NAT with nftables, IP forwarding, firewall rules, and MTU tuning.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

Before you start

  • A Linux server with a public IP address and root or sudo access
  • Linux kernel 5.6 or newer (or the wireguard-dkms backport on older kernels)
  • Basic familiarity with systemd service management
  • UDP port 51820 unblocked at the cloud provider or hardware firewall level

WireGuard is a modern VPN protocol built into the Linux kernel since 5.6. It is faster to configure than OpenVPN or IPsec, uses proven cryptography (Curve25519, ChaCha20, Poly1305), and its codebase is small enough to audit. This guide walks through a full server and client setup: key generation, server config, firewall rules with NAT, routing, and MTU tuning.

Prerequisites and Network Assumptions

The server is a VPS or dedicated machine with a public IPv4 address. The examples use 10.0.0.1/24 as the WireGuard tunnel subnet and eth0 as the public-facing interface — replace both to match your environment. All server commands run as root or with sudo.

Install WireGuard

Debian / Ubuntu

apt update && apt install -y wireguard

Fedora / RHEL 9 / Rocky Linux

dnf install -y wireguard-tools

On RHEL/Rocky the kernel module ships with the standard kernel since 5.6; wireguard-tools provides wg and wg-quick.

Arch Linux

pacman -S wireguard-tools

Generate Server Keys

WireGuard uses static Curve25519 key pairs. Keep the private key readable only by root.

cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key

Store the public key somewhere handy — every client config needs it.

Generate Client Keys

Generate one key pair per client. You can do this on the server for convenience, or on the client itself (preferred for zero-knowledge setups).

wg genkey | tee client1_private.key | wg pubkey > client1_public.key

Optionally generate a pre-shared key for an extra layer of symmetric encryption:

wg genpsk > client1_psk.key

Create the Server Configuration

Create /etc/wireguard/wg0.conf. Substitute the actual key values from the files generated above.

cat /etc/wireguard/server_private.key   # copy this value
cat /etc/wireguard/client1_public.key   # copy this value
cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>

# NAT and routing — PostUp/PreDown manage firewall rules automatically
PostUp = nft add table ip wg-nat; \
         nft add chain ip wg-nat postrouting { type nat hook postrouting priority 100 \; }; \
         nft add rule ip wg-nat postrouting oifname "eth0" masquerade
PreDown = nft delete table ip wg-nat

[Peer]
# client1
PublicKey = <CLIENT1_PUBLIC_KEY>
PresharedKey = <CLIENT1_PSK>
AllowedIPs = 10.0.0.2/32
EOF

AllowedIPs on the server side acts as an IP whitelist: only packets sourced from 10.0.0.2 are accepted from this peer. Add more [Peer] blocks for additional clients, incrementing the tunnel IP each time.

If you prefer firewalld (common on Fedora/RHEL), replace the PostUp/PreDown lines:

# firewalld alternative — add to [Interface] in place of nft lines
PostUp = firewall-cmd --add-interface=wg0 --zone=internal; \
         firewall-cmd --add-masquerade --zone=public
PreDown = firewall-cmd --remove-interface=wg0 --zone=internal; \
          firewall-cmd --remove-masquerade --zone=public

Enable IP Forwarding

The kernel must forward packets between the tunnel and the public interface. This setting persists across reboots.

echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf

For dual-stack deployments also add net.ipv6.conf.all.forwarding = 1 to the same file.

Open the Firewall Port

ufw (Debian / Ubuntu)

ufw allow 51820/udp
ufw reload

firewalld (Fedora / RHEL / Rocky)

firewall-cmd --permanent --add-port=51820/udp
firewall-cmd --reload

nftables directly

nft add rule inet filter input udp dport 51820 accept

Make this persistent by adding it to your /etc/nftables.conf ruleset.

Start the WireGuard Interface

systemctl enable --now wg-quick@wg0

The wg-quick@ systemd template reads /etc/wireguard/wg0.conf automatically. Verify the interface is up:

wg show wg0

Expected output (will vary):

interface: wg0
  public key: (your server public key)
  private key: (hidden)
  listening port: 51820

peer: (client1 public key)
  preshared key: (hidden)
  allowed ips: 10.0.0.2/32

Configure the Client

On the client machine, create /etc/wireguard/wg0.conf (Linux) or import into the WireGuard app (Android/iOS/macOS/Windows).

[Interface]
Address = 10.0.0.2/32
PrivateKey = <CLIENT1_PRIVATE_KEY>
DNS = 1.1.1.1
MTU = 1420

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <CLIENT1_PSK>
Endpoint = <SERVER_PUBLIC_IP>:51820
# Route all traffic through the VPN:
AllowedIPs = 0.0.0.0/0, ::/0
# Keep NAT alive on home routers:
PersistentKeepalive = 25

Bring the client tunnel up:

systemctl enable --now wg-quick@wg0

MTU Tuning

WireGuard adds a 60-byte overhead per packet (IPv4: 20 IP + 8 UDP + 32 WireGuard). Standard Ethernet MTU is 1500, so the safe starting value is 1420. If your ISP uses PPPoE (adds 8 bytes), drop it to 1412. Jumbo-frame links can go higher. Set the MTU in the client's [Interface] section and optionally on the server too:

# In [Interface] on either side:
MTU = 1420

Confirm no fragmentation with a ping test from the client after connecting:

ping -M do -s 1380 10.0.0.1

Increase the size until packets start dropping, then back off 20 bytes — that is your practical ceiling.

Verify End-to-End Connectivity

# From the client, ping the server tunnel IP:
ping 10.0.0.1

# Confirm your exit IP is the server's public IP:
curl -s https://ifconfig.me

# Check the handshake timestamp on the server:
wg show wg0 latest-handshakes

A successful handshake timestamp within the last few minutes confirms both sides are talking. If latest-handshakes shows 0 or no output, the tunnel has not connected — see troubleshooting below.

Adding More Clients

For each new client: generate a new key pair, assign a unique tunnel IP (10.0.0.3/32, etc.), and add a [Peer] block to the server config. Reload without dropping existing connections:

wg addconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)

Or simply restart the service if a brief interruption is acceptable:

systemctl restart wg-quick@wg0

Troubleshooting

  • No handshake: Confirm UDP 51820 is reachable from the client with nc -u -z <SERVER_IP> 51820 or check cloud provider security groups — they are a common blocker.
  • Tunnel up but no internet: IP forwarding is most likely disabled. Re-run sysctl net.ipv4.ip_forward and confirm it returns 1. Also verify the NAT rule fired with nft list table ip wg-nat.
  • DNS leaks: Set DNS = in the client [Interface] and, on systemd-resolved clients, check that resolvectl status wg0 shows the expected DNS server.
  • Packet loss / high latency: Lower the client MTU in 8-byte increments until loss stops. On PPPoE or VPN-over-VPN paths this is the most common performance issue.
  • Permission denied on key files: Key files must be mode 600 and owned by root. Run chmod 600 /etc/wireguard/*.key and retry.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can I run WireGuard without a public static IP on the server?
Yes, but clients need a way to reach the server. Use a dynamic DNS hostname (e.g. from DuckDNS) in the client Endpoint field. The server itself does not need to know its own public IP.
What is PersistentKeepalive and do I always need it?
It sends a keepalive UDP packet every N seconds so NAT mappings on home routers stay alive. Set it to 25 on clients behind NAT. Servers or peers with direct public IPs do not need it.
How do I route only specific traffic through the VPN instead of all traffic?
Replace AllowedIPs = 0.0.0.0/0 in the client config with specific subnets, for example 10.0.0.0/24 for LAN-only access. WireGuard uses AllowedIPs as both a routing and ACL table.
Is the pre-shared key mandatory?
No. It is optional but recommended — it adds a layer of symmetric encryption on top of the Curve25519 handshake, providing some protection if Curve25519 were ever broken by a future quantum algorithm.
Why does my DNS revert to the ISP DNS after connecting?
On systemd-resolved systems, the DNS line in wg-quick's config sets a per-link DNS server, but split-DNS behavior depends on the DNSSEC and routing settings. Check resolvectl status wg0 and consider setting Domains = ~. in the Interface section to force all DNS over the tunnel.

Related guides