$linuxjunkies
>

How to Set Up a VPN with WireGuard

Set up a WireGuard VPN on Linux from scratch: generate keys, configure server and client peers, enable routing, and verify a live encrypted tunnel.

IntermediateUbuntuDebianFedoraArch9 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 later on both server and client (check with uname -r)
  • UDP port 51820 reachable on the server (or choose any unused UDP port)
  • Basic familiarity with editing files and running commands as root

WireGuard is a modern VPN protocol built into the Linux kernel since 5.6. It replaces the complexity of OpenVPN and IPsec with a small, auditable codebase, fast handshakes, and straightforward configuration. This guide walks through a server/client setup: one Linux machine acts as the WireGuard server (the peer that forwards traffic), and one or more clients connect through it. You will end with a working, routed tunnel you can verify end-to-end.

Install WireGuard

On kernel 5.6+ the module ships with the kernel. You only need the userspace tools.

Debian / Ubuntu

sudo apt update && sudo apt install -y wireguard

Fedora / RHEL 9+ / Rocky 9+

sudo dnf install -y wireguard-tools

On RHEL 8 / Rocky 8 you also need the EPEL repository and the kernel module from ELRepo before running the above.

Arch Linux

sudo pacman -S wireguard-tools

Generate Key Pairs

Every peer — server and each client — needs its own private/public key pair. Generate them on each machine separately. Never copy a private key across machines.

wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
chmod 600 /etc/wireguard/privatekey

Read the values when you need them:

cat /etc/wireguard/privatekey   # keep secret
cat /etc/wireguard/publickey    # share with the other peer

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

wg genpsk > /etc/wireguard/presharedkey
chmod 600 /etc/wireguard/presharedkey

Configure the Server

The server configuration lives at /etc/wireguard/wg0.conf. Replace the placeholder values with your actual keys and the client's public key. The 10.0.0.0/24 subnet is the VPN tunnel network — pick anything not in use on either side.

cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>

# Optional: route client traffic to the internet
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  = <CLIENT_PUBLIC_KEY>
PresharedKey = <PRESHARED_KEY>   # remove line if not using
AllowedIPs = 10.0.0.2/32
EOF
chmod 600 /etc/wireguard/wg0.conf

PostUp / PostDown note: These lines handle NAT so clients can reach the internet through the server. Replace eth0 with your actual outbound interface (ip route get 1.1.1.1 will show it). If you only want a private mesh with no internet routing, remove those lines entirely.

Enable IP Forwarding

Required for the server to route packets between the tunnel and the internet.

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

Open the Firewall Port

ufw (Ubuntu/Debian)

sudo ufw allow 51820/udp
sudo ufw reload

firewalld (Fedora/RHEL/Rocky)

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

nftables (Arch or manual setup)

sudo nft add rule inet filter input udp dport 51820 accept

Configure the Client

Create /etc/wireguard/wg0.conf on the client machine. AllowedIPs = 0.0.0.0/0 routes all traffic through the tunnel (full-tunnel mode). Use 10.0.0.1/32 instead if you only want to reach the VPN subnet (split-tunnel).

cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address    = 10.0.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS        = 1.1.1.1

[Peer]
PublicKey  = <SERVER_PUBLIC_KEY>
PresharedKey = <PRESHARED_KEY>   # remove line if not using
Endpoint   = YOUR.SERVER.IP.OR.HOSTNAME:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF
chmod 600 /etc/wireguard/wg0.conf

PersistentKeepalive = 25 sends a keepalive every 25 seconds, which keeps the tunnel alive through NAT. Omit it on the server side or when the client has a public IP.

Start the Tunnel

Use wg-quick, which handles interface creation, routing, and DNS automatically.

Start once (both server and client)

sudo wg-quick up wg0

Enable on boot with systemd

sudo systemctl enable --now wg-quick@wg0

To stop the tunnel manually:

sudo wg-quick down wg0

Verify the Tunnel

On the server, check that the client has completed a handshake:

sudo wg show

You should see output similar to (values will vary):

# interface: wg0
#   public key: <SERVER_PUBLIC_KEY>
#   listening port: 51820
#
# peer: <CLIENT_PUBLIC_KEY>
#   preshared key: (hidden)
#   endpoint: 203.0.113.45:49201
#   allowed ips: 10.0.0.2/32
#   latest handshake: 4 seconds ago
#   transfer: 1.23 KiB received, 840 B sent

A "latest handshake" timestamp confirms the tunnel is live. From the client, ping the server's VPN address:

ping -c 4 10.0.0.1

For full-tunnel mode, confirm your public IP has changed:

curl -s https://ifconfig.me

Adding More Clients

Each new client follows the same key-generation and client-config steps above, using a unique VPN address (e.g., 10.0.0.3/24). Add a [Peer] block on the server for each client — no need to restart the interface:

sudo wg set wg0 peer <NEW_CLIENT_PUBLIC_KEY> allowed-ips 10.0.0.3/32

To make this survive a restart, also append the block to /etc/wireguard/wg0.conf or use wg addconf.

Troubleshooting

  • No handshake: The most common cause is a firewall blocking UDP 51820 on the server. Double-check with sudo ss -ulnp | grep 51820 to confirm WireGuard is listening, then verify your firewall rules.
  • Tunnel up but no internet: IP forwarding is not active, or the PostUp NAT rules are referencing the wrong interface. Run sysctl net.ipv4.ip_forward — it must return 1. Check the interface name with ip link.
  • DNS leaks in full-tunnel mode: The DNS line in the client config is handled by wg-quick via resolvconf or systemd-resolved. If DNS does not update, install resolvconf (Debian/Ubuntu: sudo apt install resolvconf) or ensure systemd-resolved is running.
  • Peers behind symmetric NAT: PersistentKeepalive helps, but if both peers are behind strict NAT with no public IP on either side, you need a relay (TURN-style). Consider a third server as a relay peer or look into Tailscale/Headscale which automate this.
  • Permission denied on config file: The config must be owned by root and mode 600. Run sudo chmod 600 /etc/wireguard/wg0.conf && sudo chown root:root /etc/wireguard/wg0.conf.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

What is the difference between AllowedIPs 0.0.0.0/0 and a specific subnet?
Using 0.0.0.0/0 (full-tunnel) routes all client traffic through the VPN, including internet traffic. A specific subnet like 10.0.0.0/24 (split-tunnel) only routes traffic destined for the VPN network, leaving other traffic on the local connection.
Do I need to restart WireGuard to add a new client?
No. Use sudo wg set wg0 peer <pubkey> allowed-ips <ip>/32 to add a peer live. You should also append the Peer block to wg0.conf so it persists across restarts.
Is WireGuard safe to expose directly on a public server?
Yes. WireGuard only responds to packets carrying a valid cryptographic handshake, so it is effectively invisible to port scanners. There is no version banner or unauthenticated response to probe.
Why does wg show report no handshake even though the tunnel is up?
A handshake only occurs when traffic is actually sent. Ping the remote VPN IP to trigger one. If it still fails, the most likely causes are a firewall blocking UDP 51820, a wrong server IP in the client Endpoint, or mismatched public keys.
Can WireGuard work on mobile clients?
Yes. The official WireGuard apps for Android and iOS support the same configuration format. You can generate a QR code from the client config file with qrencode -t ansiutf8 < /etc/wireguard/wg0.conf and scan it directly in the app.

Related guides