A VPN creates an encrypted tunnel between two points over an otherwise untrusted network, making traffic through it behave as if both ends were on the same private network. WireGuard is the modern default for this on Linux - a small, auditable codebase (merged directly into the kernel since 5.6) in place of the much larger attack surface of older protocols like OpenVPN or IPsec.

How it actually works at the network level

WireGuard creates a new virtual network interface (wg0, typically), the same category of thing as enp3s0 or lo covered in Networking Basics - traffic routed through that interface gets encrypted and sent to a peer, which decrypts it and either consumes it locally or routes it onward. WireGuard deliberately avoids the "client/server" language other VPNs use: every endpoint is a peer with the same configuration shape, even if one happens to act as an always-on relay for several others in practice.

Keys, not certificates

WireGuard uses simple public/private keypairs - no certificate authority, no PKI infrastructure to manage, conceptually much closer to SSH keys than to a TLS setup:

umask 077   # so the private key file isn't readable by other users
wg genkey | tee privatekey | wg pubkey > publickey

Each peer generates its own keypair once, shares its public key with the peers it wants to connect to, and keeps its private key secret - the same asymmetric-trust model as SSH's authorized_keys.

A minimal two-peer config

On peer A (/etc/wireguard/wg0.conf):

[Interface]
PrivateKey = <A's private key>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <B's public key>
Endpoint = 203.0.113.5:51820
AllowedIPs = 10.0.0.2/32

On peer B, the mirror image:

[Interface]
PrivateKey = <B's private key>
Address = 10.0.0.2/24

[Peer]
PublicKey = <A's public key>
Endpoint = 198.51.100.9:51820
AllowedIPs = 10.0.0.1/32

AllowedIPs does double duty: it's both a routing rule (traffic to this range goes to this peer) and a firewall rule (only packets appearing to originate from this range, cryptographically verified, are accepted from this peer). Setting AllowedIPs = 0.0.0.0/0 on a peer sends all of this machine's IPv4 traffic through the tunnel to that peer - the "full tunnel" VPN pattern, as opposed to routing only specific subnets (add ::/0 to do the same for IPv6).

Bringing the tunnel up

sudo wg-quick up wg0
sudo wg-quick down wg0
sudo systemctl enable --now wg-quick@wg0    # persist across reboots
sudo wg show
interface: wg0
  public key: <...>
  listening port: 51820

peer: <B's public key>
  endpoint: 203.0.113.5:51820
  allowed ips: 10.0.0.2/32
  latest handshake: 42 seconds ago
  transfer: 1.21 MiB received, 892.44 KiB sent

A recent "latest handshake" confirms the tunnel is actually active and exchanging keys correctly, not just configured.

Firewall and NAT

The listening peer needs its ListenPort (UDP, not TCP) reachable through any firewall in front of it:

sudo ufw allow 51820/udp

See Firewalls: iptables and nftables. A peer behind NAT with no port forwarding can still initiate a connection outbound to a peer with a public address - it just can't be the one waiting to receive the first packet, the same constraint that shapes most peer-to-peer networking.

WireGuard is silent when there's no traffic to send, so a NAT router eventually forgets the mapping and the public peer loses its way back in. On the peer behind NAT, add a keepalive to its [Peer] section to hold the mapping open:

PersistentKeepalive = 25

Compared to OpenVPN and IPsec

Both older protocols remain widely deployed and are more configurable in some respects (OpenVPN in particular supports certificate-based multi-user setups more directly), but come with substantially larger codebases and more configuration surface for something to be misconfigured or exploited. For a straightforward site-to-site or personal remote-access tunnel, WireGuard's small config and kernel-level performance make it the default recommendation today unless there's a specific reason to reach for the alternatives.