MHOSTMHOST

WireGuard VPN: server and clients

WireGuard is a modern VPN protocol that runs right inside the Linux kernel: a small codebase, modern cryptography, and noticeably simpler configuration than OpenVPN or IPsec. In this article you'll set up your own WireGuard server on an mHost VPS and connect a client device to it, routing that device's traffic through the server.

Diagram: a client device and the VPN server exchange traffic through an encrypted tunnel, and the server carries on to the internet

What you'll need

  • An mHost VPS running Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
  • SSH access as root or a user with sudo rights.
  • The server's public IP address (or a domain name pointing to it) — you'll need it as the Endpoint in the client config.
  • A client device with the WireGuard app installed — official clients for Windows, macOS, Linux, iOS, and Android are listed at wireguard.com/install — or another Linux server.
Tip: if you don't need to configure everything by hand, VMmanager 6 has a ready-made Wireguard VPN recipe — it brings up a VPN server in one click and immediately drops a ready client config at /etc/wireguard/client/client.conf (how to log in to the panel — see "How to log in to VMmanager 6"; the full catalog of recipes — "Ready-made VMmanager 6 recipes"). Below is the same task done by hand — useful if the recipe isn't available for your OS, you need control over the subnet, port, or number of clients, or you just want to understand how it all works under the hood.

Step 1. Install wireguard-tools

WireGuard has been built into the Linux kernel since version 5.6 — on Ubuntu 22.04/24.04, Debian 11/12, and AlmaLinux/Rocky Linux 9 there's no separate kernel module to build; you only need the wg and wg-quick userspace utilities from the wireguard-tools package.

Ubuntu and Debian

bash
sudo apt update
sudo apt install -y wireguard-tools

AlmaLinux and Rocky Linux

bash
sudo dnf install -y epel-release
sudo dnf install -y wireguard-tools
Tip: this works on version 9 — its kernel already includes native WireGuard support. On version 8 you additionally need the kernel module from ELRepo:
bash
sudo dnf install -y https://www.elrepo.org/elrepo-release-8.el8.elrepo.noarch.rpm
sudo dnf install -y kmod-wireguard
Verify: the elrepo-release package URL is tied to a specific release and may change over time — if the command above doesn't work, look up the current link for your version on elrepo.org.

Check that the utilities were installed:

bash
wg --version

Step 2. Generate the server and client keys

WireGuard uses a key pair — private and public — for each side of the tunnel. The private key is never shared with anyone; the public key is what the two sides exchange so they can recognize each other.

Create the directories for the server and client configuration and lock them down:

bash
sudo mkdir -p /etc/wireguard/client
sudo chmod 700 /etc/wireguard /etc/wireguard/client

Generate the server's key pair:

bash
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key'

And the client's key pair:

bash
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/client/client_private.key | wg pubkey > /etc/wireguard/client/client_public.key'
  • wg genkey prints a random private key to stdout; tee saves it to a file while also passing it along the pipeline; wg pubkey computes the matching public key from that private key.
  • umask 077 before creating the files guarantees 600 permissions (read/write for root only) — private keys shouldn't be readable by anyone but their owner.

You'll need the public keys in the next steps:

bash
sudo cat /etc/wireguard/server_public.key
sudo cat /etc/wireguard/client/client_public.key
Important: the private keys (server_private.key, client_private.key) each stay on their own side — the server only ever uses its own, and the client only ever uses its own. Never copy a private key to the other side; that's both unnecessary and unsafe. Public keys, by contrast, aren't secret and are meant to be shared openly.

Step 3. Configure the server: wg0.conf

Find out which network interface the server uses to reach the internet — you'll need it for the NAT rules below:

bash
ip route get 1.1.1.1

The output will look like 1.1.1.1 via <gateway> dev eth0 src <server IP> — the name after dev (often eth0, but you'll also see ens3, enp1s0, and similar) is the interface you need. The examples below use eth0 — substitute your own value if it differs.

Create the server config:

bash
sudo nano /etc/wireguard/wg0.conf
ini
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# client
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.8.0.2/32
  • Address — the server's own address inside the VPN subnet 10.8.0.0/24.
  • ListenPort — the UDP port WireGuard listens on; the standard WireGuard port is 51820, which we'll open in the firewall in Step 6.
  • PrivateKey — replace <SERVER_PRIVATE_KEY> with the contents of /etc/wireguard/server_private.key.
  • PostUp/PostDown — bash commands run when the interface comes up and goes down (%i expands to the interface name, wg0): they allow packets to be forwarded between the tunnel and the main interface (iptables -A FORWARD) and rewrite the client's source address to the server's address on the way out to the internet (MASQUERADE in the nat table) — without this, return packets from the internet wouldn't find their way back into the private 10.8.0.0/24 subnet.
  • [Peer] — the client's public key and its tunnel address (AllowedIPs): this is both a permission (only accept packets from this peer if they come from 10.8.0.2) and a route (send packets addressed to 10.8.0.2 to this specific client).

Lock down the file's permissions — it holds the server's private key:

bash
sudo chmod 600 /etc/wireguard/wg0.conf
Tip (AlmaLinux/Rocky): some minimal templates don't have the iptables command at all — if the next step fails with iptables: command not found, install the package: sudo dnf install -y iptables-nft.

Step 4. Enable the WireGuard interface

bash
sudo systemctl enable --now wg-quick@wg0

This brings up the wg0 interface right away (it effectively runs wg-quick up wg0, including your PostUp rules) and enables it to start automatically on reboot — the unit name wg-quick@wg0 is built from the config file name /etc/wireguard/wg0.conf.

Check that everything came up:

bash
sudo systemctl status wg-quick@wg0
sudo wg show

wg show should display the wg0 interface, port 51820, and the client you added under peer — with no latest handshake yet; that appears once the client actually connects (Step 7).

Important: if you edit wg0.conf after the first start, sudo systemctl restart wg-quick@wg0 recreates the interface from scratch and briefly drops every current connection. To apply changes without a disruption, use sudo wg syncconf wg0 <(sudo wg-quick strip wg0) — it applies only what changed, without recreating the interface.

Step 5. Enable IP forwarding

Without this setting, the kernel won't forward packets between the wg0 interface and the server's outbound interface at all, and the PostUp rules from Step 3 won't have any effect. Turn on forwarding persistently, via a dedicated file under /etc/sysctl.d/:

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

Check the value:

bash
sysctl net.ipv4.ip_forward
Done: the command should print net.ipv4.ip_forward = 1.

Step 6. Open the WireGuard port in ufw

WireGuard listens on UDP, not TCP — don't mix up the protocol when opening the port. For more on ufw itself, see "The ufw firewall: managing ports".

bash
sudo ufw allow 51820/udp
sudo ufw status
Done: the output of sudo ufw status includes a line for port 51820/udp with status ALLOW, and Status: active confirms the firewall is on.
AlmaLinux/Rocky Linux uses firewalld by default instead of ufw — the commands are different (firewall-cmd); open port 51820/udp the equivalent way there, since this article doesn't cover firewalld.

Step 7. Set up the client and verify the connection

Create the client config — at the same path the ready-made recipe uses:

bash
sudo nano /etc/wireguard/client/client.conf
ini
[Interface]
Address = 10.8.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS = 1.1.1.1

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
  • PrivateKey — the contents of /etc/wireguard/client/client_private.key.
  • PublicKey under [Peer] — the contents of /etc/wireguard/server_public.key.
  • Endpoint — your server's public IP address (or domain) and port 51820; replace 203.0.113.10 with your own.
  • AllowedIPs = 0.0.0.0/0 routes all of the client's traffic through the tunnel — a full VPN, not just access to the server's subnet. If you only need access to the internal 10.8.0.0/24 subnet without routing all other traffic, use that instead of 0.0.0.0/0.
  • PersistentKeepalive = 25 — the client sends a keepalive every 25 seconds; useful if the client itself sits behind NAT (a home router, a mobile network) — without it, the server may not be able to reach the client first.
bash
sudo chmod 600 /etc/wireguard/client/client.conf
Tip: if bringing up the interface on Ubuntu/Debian fails with resolvconf: command not found, that's because of the DNS field, which requires the resolvconf or openresolv package. Install it: sudo apt install -y openresolv.

Transfer the file to the client device, for example over scp (run this from the client device itself, replacing 203.0.113.10 with your server's IP):

bash
scp root@203.0.113.10:/etc/wireguard/client/client.conf .
  • If the client is also Linux: copy the file to /etc/wireguard/wg0.conf and enable the interface the same way you did on the server — sudo systemctl enable --now wg-quick@wg0.
  • If the client is the official WireGuard app for Windows, macOS, iOS, or Android: import client.conf via "Import tunnel(s) from file." On a phone it's often easier to scan a QR code instead: sudo apt install -y qrencode, then qrencode -t ansiutf8 < /etc/wireguard/client/client.conf.
Important: once you've transferred client.conf to the client device, delete the client's private key from the server (sudo rm /etc/wireguard/client/client_private.key) — the server doesn't need it to function, and there's no reason to keep an extra copy of someone else's private key around.

Bring up the tunnel on the client and check on the server that a handshake happened:

bash
sudo wg show wg0
interface: wg0
  public key: <SERVER_PUBLIC_KEY>
  private key: (hidden)
  listening port: 51820

peer: <CLIENT_PUBLIC_KEY>
  endpoint: 198.51.100.20:51280
  allowed ips: 10.8.0.2/32
  latest handshake: 1 minute, 5 seconds ago
  transfer: 1.42 KiB received, 4.68 KiB sent
Done: a latest handshake line means the client connected successfully and the tunnel is working. To confirm traffic is actually going through the server, run curl ifconfig.me on the client — it should return the server's public IP address, not the client's.

What's next

To add another client: repeat Step 2 for a new key pair, add another [Peer] block to the server's wg0.conf with a free address in the subnet (for example, 10.8.0.3/32), and apply the change without dropping the tunnel — sudo wg syncconf wg0 <(sudo wg-quick strip wg0) (see Step 4). Create that client's config the same way as in Step 7, under its own name (for example, client2.conf).

Want a more familiar VPN option instead of WireGuard? VMmanager 6 has a ready-made Openvpn recipe — see "OpenVPN: server and clients".