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.
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 51820to 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 return1. Check the interface name withip link. - DNS leaks in full-tunnel mode: The
DNSline in the client config is handled bywg-quickviaresolvconforsystemd-resolved. If DNS does not update, installresolvconf(Debian/Ubuntu:sudo apt install resolvconf) or ensuresystemd-resolvedis running. - Peers behind symmetric NAT:
PersistentKeepalivehelps, 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.
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
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.