How WireGuard actually works (and how to set it up)

Telesphoreo

What is WireGuard?

WireGuard is a VPN. But before your eyes glaze over, let’s talk about what that actually means, because “VPN” has been ruined by marketing.

A VPN is a private tunnel between two computers over the internet. When your PC connects to your cloud server over a VPN, it’s like running a really long invisible ethernet cable between them. The two machines get private IP addresses that only they know about, and they can talk to each other as if they were on the same local network. The data going through that tunnel is encrypted, so anyone snooping on the traffic in between sees nothing useful.

The reason WireGuard specifically is worth using over older options like OpenVPN or IPsec is that it does all of this with way less code and complexity. The entire WireGuard codebase is roughly 4,000 lines, while OpenVPN is over 100,000. Less code means fewer places for bugs and security vulnerabilities to hide.

Why should you care?

If you have a cloud server, it’s sitting on the public internet with its SSH port wide open. Every server with a public IP gets hammered by botnets trying random username and password combinations against SSH around the clock. You can check this yourself by running journalctl -u sshd on your server, and you’ll probably see hundreds or thousands of failed login attempts from IPs you’ve never seen before.

Once you have WireGuard set up, you can configure SSH to only listen on the WireGuard interface (10.0.0.1) instead of the public IP, which means SSH is no longer reachable from the internet at all. Botnets can’t even attempt to log in because port 22 simply doesn’t respond on the public IP anymore. The only way to SSH into the server is through the WireGuard tunnel, and that requires having the correct private key on your machine. No key, no tunnel, no SSH.

On top of that, you can run services on your server that are only accessible through the tunnel. Things like database admin panels, internal dashboards, monitoring tools. Stuff you need access to but definitely don’t want exposed to the internet. Instead of messing with IP allowlists that break when your home IP changes, you just bind those services to the WireGuard IP and call it a day.

How WireGuard actually works

This is the part most tutorials skip, and it’s the part that makes everything else make sense.

Keys, not usernames and passwords

WireGuard doesn’t use usernames or passwords. It uses public key cryptography, the same kind of thing that makes HTTPS work.

The way it works is you generate a key pair: a private key and a public key. They’re mathematically linked, so your private key can encrypt things that only your public key can decrypt, and vice versa. The one rule you need to remember is that your private key never leaves the machine it was generated on. Your public key, on the other hand, gets shared freely.

So when you set up WireGuard between two machines, each machine generates its own key pair, keeps its private key to itself, and gives its public key to the other machine. After that, both machines can encrypt traffic that only the other can read, and nobody in between can decrypt it, even if they capture every single packet.

The tunnel is a network interface

When WireGuard starts on your machine, it creates a virtual network interface, typically called wg0. It works just like a real network adapter (like your ethernet or Wi-Fi card), except it’s entirely in software. You assign it a private IP address, like 10.0.0.1, which gives your machine a new “network card” that happens to send its traffic through an encrypted tunnel to the other side.

So when your machine wants to send a packet to 10.0.0.2, it goes: “Oh, that matches my wg0 interface.” It hands the packet to WireGuard, which encrypts it, wraps it in a normal UDP packet, and sends it over the real internet to the other machine. The other machine’s WireGuard then unwraps it, decrypts it, and delivers it as if it arrived on a local network.

CryptoKey routing

This is the clever part, and the one worth understanding.

In the config file, each peer has a field called AllowedIPs. Most tutorials just tell you what to put there without explaining what it does. The thing is, AllowedIPs serves two purposes at once, and understanding that makes the whole config click.

On the sending side, AllowedIPs acts as a routing table. If your machine wants to send a packet to an IP, WireGuard checks which peer has that IP in their AllowedIPs, and that’s the peer the packet gets encrypted and sent to. If no peer claims that IP, the packet doesn’t go through the tunnel at all.

On the receiving side, it works as an access control list. When an encrypted packet arrives from a peer, WireGuard decrypts it and checks the source IP. If that IP isn’t in the peer’s AllowedIPs, the packet gets dropped, which prevents a peer from spoofing traffic as a different IP.

So AllowedIPs is simultaneously answering “Where should I send traffic for this IP?” and “Is this peer allowed to send traffic from this IP?” WireGuard calls this CryptoKey Routing, and it’s the reason the configuration ends up being so simple compared to other VPNs.

UDP and statelessness

WireGuard runs over UDP, not TCP, which means there’s no “connection” in the traditional sense. There’s no handshake that says “OK, we’re now connected.” Instead, when one side sends an encrypted packet and the other side successfully decrypts it, they know the tunnel is working. And if no traffic flows for a while, there’s nothing to “time out” because there was never a connection to begin with. The tunnel is just there, ready to work whenever a packet shows up.

This is also why WireGuard handles network changes well, like switching from Wi-Fi to cellular. Since there’s no connection state to maintain, there’s nothing to break.

What we’re building

Here’s our setup. A cloud server running Debian or Ubuntu will be the WireGuard “server,” since it has a public IP address that your PC can reach directly, and it’ll listen on a UDP port for incoming WireGuard traffic. Your PC (Windows or macOS) will be the “client” that connects to the cloud server’s public IP and port.

I put “server” and “client” in quotes because WireGuard doesn’t actually have a concept of servers and clients. Both sides are just “peers.” But in practice, the machine with the public IP that listens for connections acts like the server, and the machine that initiates the connection acts like the client.

After we’re done, your PC and your cloud server will be able to talk to each other over private IPs (10.0.0.1 and 10.0.0.2) through an encrypted tunnel. You’ll be able to SSH into your cloud server using 10.0.0.2, access web services on it, and the connection will be protected even on sketchy coffee shop Wi-Fi.

Prerequisites

  • A cloud server running Debian 13 or Ubuntu 24.04+ with root/sudo access
  • The ability to open a UDP port in your cloud provider’s firewall (we’ll use 51820)
  • A PC running Windows or macOS
  • Basic comfort with a terminal (you don’t need to be an expert, I’ll explain everything)

Part 1: Setting up the cloud server

SSH into your cloud server. Everything in this section happens there.

Install WireGuard

sudo apt update && sudo apt install wireguard

On modern kernels (5.6+), WireGuard is actually built into the kernel itself, so this package mostly just gives you the userspace tools to configure it.

Generate keys

wg genkey | tee /dev/stderr | wg pubkey

This prints two lines: the first is your private key, and the second is your public key. Copy both somewhere safe and label them clearly. I’m serious about labeling them, because when you have four keys floating around (two per machine), it’s easy to mix them up, and a mixed-up key means the tunnel silently won’t work.

Here’s what that command is actually doing. wg genkey generates a random private key, then tee /dev/stderr prints it to your screen while also passing it along the pipeline, and finally wg pubkey reads the private key from the pipeline and mathematically derives the public key from it.

Your private key never leaves this machine. Your public key will go into the PC’s config later.

Create the server config

sudo nano /etc/wireguard/wg0.conf

Paste this in, replacing the placeholder values:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <your server private key>

[Peer]
PublicKey = <your PC's public key>
AllowedIPs = 10.0.0.2/32

Don’t worry that you don’t have the PC’s public key yet. We’ll come back and fill it in. Here’s what each line is doing.

Address = 10.0.0.1/24 is the private IP address the server will have inside the WireGuard tunnel. The /24 means “this machine is part of the 10.0.0.0 - 10.0.0.255 network.” Think of it like giving this machine an IP address on a virtual LAN that only exists between your WireGuard peers.

ListenPort = 51820 is the real UDP port WireGuard listens on. When your PC wants to connect, it sends encrypted UDP packets to your server’s public IP on this port. 51820 is the conventional WireGuard port, but you can use whatever you want.

PrivateKey is the server’s private key, the one you just generated. This is how the server decrypts packets that were encrypted with its public key. If anyone gets this key, they can impersonate your server, so keep it safe and make sure the config file isn’t world-readable (we’ll handle that below).

Then in the [Peer] section, PublicKey is the public key of the PC. This tells the server “I expect to communicate with a peer who has this public key,” and if a packet arrives that isn’t signed by this key, it gets ignored.

And AllowedIPs = 10.0.0.2/32 is where CryptoKey Routing from earlier comes in. This tells the server that the peer with this public key is allowed to identify as 10.0.0.2, and any traffic the server needs to send to 10.0.0.2 should be encrypted and sent to this peer. The /32 means this exact IP only, as opposed to a range.

Now lock down the permissions on that file:

sudo chmod 600 /etc/wireguard/wg0.conf

This makes it so only root can read it, which matters since it has your private key in it. You don’t want other users or processes on the system reading it.

Open the firewall port

You need to open UDP port 51820 inbound so your PC can reach WireGuard. Most cloud servers don’t have a firewall running on the server itself by default, but it’s worth checking before you blindly add rules.

Start by checking if ufw is active:

sudo ufw status

If it says “active”, then you have ufw running and you need to add a rule for WireGuard:

sudo ufw allow 51820/udp

If it says “inactive”, then ufw isn’t managing your firewall. The next thing to check is whether you have iptables rules instead:

sudo iptables -L -n

If the output shows a bunch of rules with ACCEPT/DROP/REJECT targets, you have iptables rules in place and you’ll need to add one:

sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT

If iptables shows mostly empty chains with a default policy of ACCEPT, then you don’t have a software firewall blocking anything on the server and you can skip this.

The other thing to be aware of is that some cloud providers have their own firewall that sits in front of your server at the network level (Hetzner, DigitalOcean, AWS security groups, etc.). If your provider has one of these, you’ll need to add an inbound rule for UDP 51820 there too. Check your provider’s control panel, and if you don’t see a firewall section or you never set one up, you’re probably fine.

Part 2: Setting up your PC

Download the official WireGuard application from wireguard.com/install. There are apps for both Windows and macOS, so grab the one for your system, install it, and open it.

Click “Add Tunnel” then “Add empty tunnel…” (on macOS it’s the + button in the bottom left, then “Add Empty Tunnel…”). The app will automatically generate a key pair for you, and the public key shows up at the top of the window. Copy that public key and go back to your cloud server to paste it into the [Peer] section’s PublicKey field in /etc/wireguard/wg0.conf.

Then in the configuration editor, replace everything with:

[Interface]
Address = 10.0.0.2/24
PrivateKey = <it should already be filled in>

[Peer]
PublicKey = <your server's public key>
Endpoint = <your server's public IP>:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

Give the tunnel a name (like “cloud-server”) and save it.

What the client config means

The differences from the server config matter, so let’s go through this one too.

Address = 10.0.0.2/24 is this PC’s IP address inside the tunnel. It’s different from the server’s 10.0.0.1 because each peer needs a unique IP, just like devices on a regular network.

PrivateKey is the PC’s private key. Same idea as the server, stays on this machine forever.

Endpoint = <IP>:51820 is the big difference between the client and server configs. The client config has an Endpoint but the server config doesn’t, and the reason for that is the endpoint tells your PC where to send WireGuard packets: your server’s real, public IP address and port. The server doesn’t need an endpoint for the PC because your PC is probably behind a home router (NAT) and doesn’t have a stable public IP. Instead, the server just learns where the PC is when the PC sends its first packet.

AllowedIPs = 10.0.0.1/32 means “route traffic for 10.0.0.1 through this tunnel, and only accept traffic from this peer if it claims to be from 10.0.0.1.” Since we only put the server’s tunnel IP here, your regular internet traffic still goes through your normal connection, and only traffic destined for the WireGuard network goes through the tunnel. This is called a split tunnel.

Then there’s PersistentKeepalive = 25. Remember how WireGuard is stateless and there’s no “connection?” Well, your home router doesn’t know that. Your router uses NAT (Network Address Translation) to let multiple devices share one public IP, and NAT keeps a table of active connections so it knows how to route responses back to the right device. If no traffic flows for a while, your router drops the NAT mapping, which means incoming packets from the server can no longer reach your PC.

What PersistentKeepalive does is tell your PC to send a tiny keepalive packet every 25 seconds, just to keep that NAT mapping alive. Without it, the connection can go stale if you’re behind NAT. The server doesn’t need this setting because it has a public IP and isn’t behind NAT.

Part 3: Go back and fill in keys

If you haven’t already, make sure you’ve swapped public keys between the two machines. The server’s wg0.conf [Peer] section should have the PC’s public key, and the PC’s wg0.conf [Peer] section should have the server’s public key.

The way to think about it is that each machine’s [Interface] section has its own private key, while each machine’s [Peer] section has the other machine’s public key. Private keys stay home, public keys get shared.

Server config:                    PC config:
[Interface]                       [Interface]
PrivateKey = <server private>     PrivateKey = <PC private>

[Peer]                            [Peer]
PublicKey = <PC public>           PublicKey = <server public>

Part 4: Bring up the tunnel

On the server

sudo wg-quick up wg0

You should see output showing the interface being created and the address being assigned. If you see errors, double check that your config syntax is correct, since a common mistake is extra whitespace or missing keys.

On your PC

In the WireGuard app, select your tunnel and click “Activate”.

Test it

From your PC, open a terminal (Command Prompt, PowerShell, or Terminal on macOS) and ping the server through the tunnel:

ping 10.0.0.1

And from the server, ping your PC:

ping 10.0.0.2

If you see replies, the tunnel is working. If you see nothing, here are the common issues to check.

The most likely problem is that keys are mixed up. Make sure public key A is in the [Peer] on machine B and vice versa, not the other way around, and definitely not private keys in peer sections.

It could also be a firewall issue. Is UDP 51820 actually open on the server? You can try sudo ss -ulnp | grep 51820 on the server to verify WireGuard is listening.

Another common one is typos in the endpoint. Make sure the PC’s config has the correct public IP and port of the server.

And if the config file has wrong permissions or can’t be read, running sudo wg show will tell you the current state of the interface and which peers are configured.

Check the status

On the server, run:

sudo wg show

This prints the current state of the WireGuard interface, including each peer, their public key, the endpoint it last communicated with, how much data has been transferred, and when the last handshake was. If you see a recent handshake time and non-zero transfer bytes, everything is working.

Part 5: Making it permanent

Right now, if you reboot the server, the tunnel goes away. To fix that:

On the server

sudo systemctl enable wg-quick@wg0

This tells systemd to bring up the wg0 interface on every boot. The @wg0 part refers to the config file name (wg0.conf), so if you named your config something else, use that name instead.

On your PC

In the WireGuard app, there’s a checkbox to launch WireGuard on startup and an option to auto-activate the tunnel.

Part 6 (optional): Adding a preshared key

WireGuard’s encryption is already strong, but if you want an extra layer you can add a preshared key. This adds symmetric encryption on top of the public key encryption, which provides post-quantum resistance. What that means is even if someone eventually builds a quantum computer that can break public key cryptography, they still can’t decrypt your traffic if you used a preshared key.

Generate one on the server:

wg genpsk

Copy the output and add this line to the [Peer] section on both the server and the PC, underneath the PublicKey line:

PresharedKey = <the key you just generated>

The preshared key must be identical on both sides. Unlike public/private keys which are different per machine, the preshared key is a shared secret between the two peers. Once you’ve added it to both configs, reload the server:

sudo wg-quick down wg0 && sudo wg-quick up wg0

And on your PC, deactivate and reactivate the tunnel in the WireGuard app.

Adding more clients

You don’t need to repeat any of the server setup. The server’s keys, config, and firewall rules are already done, and you don’t generate new keys on the server either. It keeps the same key pair it already has.

All you do is set up the new device the same way you did in Part 2: install the WireGuard app, create a tunnel, and let it generate a key pair. From there you just make two small changes.

On the new device, use the next IP in the range. If your first PC is 10.0.0.2, the next device would be 10.0.0.3, and so on. The rest of the config is the same: same server public key, same endpoint, same PersistentKeepalive.

Then on the server, add another [Peer] block to /etc/wireguard/wg0.conf with the new device’s public key and its AllowedIPs. So if you’re adding a third device, you’d add:

[Peer]
PublicKey = <new device's public key>
AllowedIPs = 10.0.0.3/32

Then reload the server config:

sudo wg-quick down wg0 && sudo wg-quick up wg0

That’s it. The server can have as many [Peer] blocks as you want, and each one is independent.

What you’ve got now

Your PC and cloud server have a private, encrypted tunnel between them. Traffic between 10.0.0.1 and 10.0.0.2 is encrypted with keys that only those two machines have, and the encryption happens at the kernel level with minimal overhead. There’s no central authority, no account to sign into, no subscription. Just two machines with each other’s public keys.

You can SSH into your server at 10.0.0.2 instead of its public IP, and you can access services that aren’t exposed to the internet through it. And if you want to go further, you can now change your server’s SSH config to only listen on 10.0.0.1 instead of 0.0.0.0, which takes SSH off the public internet entirely. No more botnet login attempts.

Copyright © 2016 - 2026 Telesphoreo. All rights reserved.