WireGuard from first principles
Why WireGuard looks the way it does: Noise_IK, cryptokey routing, cookies, timers, and the design tradeoffs behind the modern minimalist VPN.
WireGuard reads like a protocol designed by someone who had spent ten years operating IPsec and OpenVPN deployments and decided to write down everything they wished those protocols hadn't done. The result is small enough to read end-to-end in an afternoon: roughly 4,000 lines of kernel C in the Linux implementation, four packet types on the wire, one fixed cryptosuite, no negotiation, no algorithm agility, no separate control daemon.
The minimalism is the point. Every protocol decision was made by asking "can we leave this out?" and only including it when the answer was no. That stance produces a VPN that is faster than its predecessors, drastically easier to audit, and considerably less flexible — all by design. The flexibility you lose is supposed to be reclaimed at higher layers, where richer policy belongs.
This module is the canonical protocol-design explainer for WireGuard. We're not going to cover how to spin up a server (the practical guide already lives at self-hosted-wireguard-2026). We're going to look at why the protocol looks the way it does: the primitive choices, the Noise_IK handshake derivation, cookie-based DoS resistance, cryptokey routing as both feature and footgun, timer state that makes a UDP protocol feel connection-oriented, and the deliberate omissions that push identity federation, policy distribution, and routing coordination above the protocol layer.
Prerequisites
tcp-at-the-wire-level— for contrast with how a stateful connection-oriented protocol is built.nat-traversal-and-end-to-end-principle— WireGuard's UDP design assumes the realities of NAT.stream-ciphers-and-aead-construction— ChaCha20-Poly1305 is the data-channel cipher.noise-protocol-framework— WireGuard is Noise_IK plus a cookie reply, plus timers, plus framing.
Learning objectives
- Explain WireGuard's protocol design choices from a designer's perspective: why each primitive, why Noise_IK, why cookies, why no algorithm negotiation.
- Trace the four-message protocol — initiation, response, optional cookie reply, transport data — and the timer state that drives rekeying and liveness.
- Explain cryptokey routing as both the elegance and the sharp edge of WireGuard's access-control model.
- Compare WireGuard's lean design with the larger flexibility surfaces of IPsec and OpenVPN, and identify what the omissions push to higher layers.
Why WireGuard threw so much away
The thesis is that protocol minimalism is itself a security property. Every additional primitive is an additional implementation surface to get wrong. Every negotiable algorithm is a potential downgrade vector. Every configuration knob is a misconfiguration opportunity. Every TLV-style optional field is parser logic that needs to handle malicious encodings. The biggest exploitable bugs in real deployed VPN protocols over the past two decades have lived in negotiation logic, optional-field parsing, certificate-chain validation, and cipher-flexibility code paths — almost never in the well-understood crypto primitives themselves.
Jason Donenfeld's NDSS 2017 paper makes the design philosophy explicit: ship one cryptosuite, refuse to negotiate, refuse to add knobs, and rely on protocol-version replacement (rather than in-band negotiation) for crypto upgrades. If ChaCha20-Poly1305 is ever broken, the response will be WireGuard 2 with a different cipher and a wire-incompatible header — not a "use this cipher instead" option in the existing handshake. The lack of negotiation is enforced by having no field in which to negotiate.
This is uncompromising in a way that matters. There is no "enable downgrade for legacy peers" option, because there is no concept of legacy peers within a single protocol version. There is no "use AES-GCM instead" option, because ChaCha20-Poly1305 was chosen and the choice doesn't admit alternatives. There is no certificate validation logic to subvert, because there are no certificates. The protocol's attack surface is dramatically smaller than its predecessors' precisely because so much surface was eliminated by refusing to provide it.
The cost of this stance is real and worth naming honestly:
- No identity federation in-protocol. WireGuard knows public keys, not identities. Mapping public keys to humans, devices, departments, or revocable credentials is something you build above the protocol — for teams of any size, this means using a control-plane system like Tailscale or Headscale.
- No certificate revocation, no expiry, no PKI. A peer's public key authorizes them forever, until you remove it from the responder's peer list. Operationally this means key compromise requires manually removing peers; there's no CRL to publish.
- No native support for many small short-lived peers. Each peer is configuration; configuration is push-or-poll-managed by you. WireGuard works fine with 1,000 peers but it isn't designed to handle the IoT-style "10,000 ephemeral devices" problem without a control plane on top.
- Routing and authorization are the same field. This is
AllowedIPs. We'll get to why that's both elegant and dangerous.
If those tradeoffs are wrong for your environment, you build a control plane on top, or you use a different VPN. They aren't bugs; they're consequences of the minimalism.
The primitive set
WireGuard's cryptosuite is fixed at protocol-version 1. Every implementation supports exactly these primitives and only these primitives:
- Curve25519 — Diffie-Hellman key exchange. Defined in RFC 7748. Public keys are 32 bytes. ECDH operations are constant-time and don't leak through cache-timing side channels in correctly-written implementations.
- ChaCha20-Poly1305 — AEAD cipher for the data channel. Defined in RFC 8439. Stream cipher with a polynomial-MAC integrity check, fast in software without AES-NI hardware acceleration. Critical on phones, embedded devices, and any platform where AES hardware isn't available.
- BLAKE2s — keyed and unkeyed hashing. 256-bit output, faster than SHA-256 in software, used for both transcript hashing and as the hash inside HKDF.
- HKDF — key derivation. Defined in RFC 5869. Extract-then-expand structure that mixes inputs from successive DH operations into the chaining key and derives traffic keys from it.
- TAI64N — 12-byte timestamp format used as the handshake initiator's identity-binding timestamp. Provides anti-replay for the initiation message.
The justification for each choice in the design paper is "performant in software, conservative cryptanalytic margin, well-studied, fits on the wire compactly." Notably absent: AES (because Curve25519's elliptic-curve choice already commits to non-NIST curves; consistency with ChaCha eliminates one more primitive), SHA-2 (BLAKE2s is faster), elliptic-curve signatures (no signing happens — the handshake is pure DH-based authentication, see Noise_IK below). The cryptosuite is chosen to be implementable correctly in a few hundred lines of code total.
The post-quantum question is handled by the optional pre-shared key field in the handshake (covered later), not by changing the primitive set. PQ-grade DH (e.g., ML-KEM) would change the wire format and so will be a WireGuard 2 protocol bump, not a negotiable upgrade. See post-quantum-cryptography-in-transit for the broader PQ migration context.
Noise_IK in WireGuard clothing
WireGuard's handshake is the Noise protocol framework's IK pattern, with WireGuard-specific framing and additional protections. Noise_IK is the right pattern for WireGuard's deployment model: the initiator already knows the responder's static public key (because peer keys are exchanged out of band when configuring the tunnel), so a 1-RTT mutual-authentication handshake is achievable.
The Noise_IK pattern in shorthand:
-> s (pre-message: responder's static pubkey is pre-known)
<- s (pre-message: initiator's static pubkey will be sent encrypted)
...
-> e, es, s, ss (initiation)
<- e, ee, se (response)
Reading the tokens:
e— sender contributes a fresh ephemeral keypair; pubkey appears in the message.s— sender's static pubkey is included (encrypted under the current chaining key).es,ss,ee,se— Diffie-Hellman operations between sender's ephemeral or static and recipient's ephemeral or static, with the result mixed into the chaining key.
After both messages, both sides have derived the same chaining key from a sequence of DH operations involving each side's static and ephemeral keys. From that chaining key, each side derives its sending key for the data channel. The protocol provides:
- Mutual authentication. Each side proves possession of the private key matching its static public key. A man-in-the-middle who doesn't know either static private key cannot complete the handshake.
- Forward secrecy. Ephemeral keys are discarded after the handshake. Compromise of static keys later does not retroactively decrypt session traffic.
- Identity hiding for the initiator. The initiator's static public key is sent encrypted, so a passive observer doesn't learn who initiated. (The responder's static is pre-known to the initiator out of band, so identity hiding is asymmetric — the responder is presumed to be a known fixed endpoint, and the initiator is the one whose identity benefits from being concealed on the wire.)
- Replay protection of the initiation. A monotonically-increasing TAI64N timestamp is sent encrypted in the initiation; the responder rejects timestamps it has already seen from this initiator.
The wire format adds a fixed message-type byte, a sender index (the responder's chosen 4-byte session identifier sent with the response so the initiator can address future packets to the right session state), MACs for cookie/DoS handling (next section), and the Noise payload itself. The total initiation message is 148 bytes; the response is 92 bytes. After both, transport data flows.
Concrete byte layout of the initiation message:
| 0x01 | message type: HandshakeInitiation (1 byte)
| reserved | reserved zero bytes (3 bytes)
| sender index | initiator's session ID (4 bytes)
| ephemeral pub | initiator's ephemeral Curve25519 pubkey (32 bytes)
| static (enc) | AEAD-encrypted initiator static pubkey (32 + 16 byte tag)
| timestamp (enc) | AEAD-encrypted TAI64N timestamp (12 + 16 byte tag)
| MAC1 | BLAKE2s(responder's static pubkey || ...) (16 bytes)
| MAC2 | cookie-related MAC, zeros if no cookie (16 bytes)
The response message:
| 0x02 | message type: HandshakeResponse (1 byte)
| reserved | reserved (3 bytes)
| sender index | responder's session ID (4 bytes)
| receiver index | echoes initiator's sender index (4 bytes)
| ephemeral pub | responder's ephemeral Curve25519 pubkey (32 bytes)
| empty (enc) | AEAD-encrypted empty payload (0 + 16 byte tag) - confirms key knowledge
| MAC1 | BLAKE2s MAC (16 bytes)
| MAC2 | cookie MAC (16 bytes)
The empty-but-tagged AEAD payload in the response is the responder's proof that it derived the same chaining-key state as the initiator: if the tag verifies, the initiator knows the responder genuinely possesses the static private key matching the pre-known responder static pubkey.
After response acceptance, both sides derive two transport keys (Tsend_initiator, Tsend_responder) from the final chaining key via HKDF. Each side encrypts outbound transport packets with its own send key and decrypts inbound packets with the peer's send key. Sender index numbers identify which session the packet belongs to (a peer might briefly have two valid sessions during rekey overlap).
Cookies and DoS resistance
The handshake imposes asymmetric work on the responder. To process an initiation message and produce a response, the responder performs:
- Two Curve25519 DH operations (for the
esandssmixing). - One AEAD decryption to extract the initiator's static pubkey.
- One AEAD decryption to extract the timestamp.
- Lookups against the peer table to verify the static pubkey is authorized.
- Generation of a fresh ephemeral.
- One Curve25519 DH operation for
ee(for the response). - One Curve25519 DH operation for
se(for the response). - AEAD encryption of the empty response payload.
Curve25519 operations dominate — modern x86 hardware does maybe 100,000 of these per core per second. That sounds plenty, until you consider that an attacker can send unsolicited initiation messages from forged source addresses and force the responder to do all that work for each one before discovering the forgery has no follow-through. A modest unauthenticated flood can saturate a server's CPU.
WireGuard's defense is the cookie reply, which is a stateless rate-limiting mechanism. The flow:
- Normal operation: Initiator sends
HandshakeInitiation. Responder is not under load. It processes the initiation normally and replies withHandshakeResponse. - Under load: Initiator sends
HandshakeInitiation. Responder detects load (CPU above threshold, handshake rate exceeded). Instead of doing the full processing, the responder sends aCookieReplymessage containing a cookie derived from a server-only secret, the initiator's claimed source IP/port, and a 16-byte MAC over the initiator's MAC1 field. - Initiator retries: Initiator receives the cookie reply, includes the cookie value (as MAC2) in a new
HandshakeInitiation. The MAC2 is computed using the cookie as a key. - Responder verifies cheaply: When a new initiation arrives with a non-zero MAC2, the responder verifies MAC2 using the cookie it would have generated for that source IP. This is a single BLAKE2s MAC operation — trivial CPU compared to a Curve25519 operation. If MAC2 verifies, the responder proceeds with full handshake processing.
The point: an attacker spoofing source addresses cannot receive the cookie reply (because the responder sends it to the spoofed address, not the attacker's actual address). Without the cookie, MAC2 verification fails, and the responder discards the initiation cheaply. This forces the attacker to use real source addresses, which a) is far less anonymous and b) limits the per-address request rate the responder will accept.
The cookie itself is derived from a secret that rotates every two minutes, so cookies have a bounded lifetime. Cookies are bound to source IP, so they can't be reused from a different source. The whole mechanism adds one packet type, one MAC field, and a few hundred lines of code — orders of magnitude less protocol surface than IKE's elaborate puzzle/SPI-cookie negotiation.
The CookieReply message wire format:
| 0x03 | message type: CookieReply (1 byte)
| reserved | reserved (3 bytes)
| receiver index | echoes the initiator's sender index (4 bytes)
| nonce | XChaCha20 nonce (24 bytes)
| cookie (enc) | AEAD-encrypted cookie (16 + 16 byte tag = 32 bytes)
The cookie is encrypted under a key derived from the responder's static pubkey, so only the legitimate sender of the original initiation (which knows the responder's static pubkey out of band) can decrypt it.
Cryptokey routing
This is the design idea that makes WireGuard's policy model both elegant and dangerous. Cryptokey routing collapses two concepts into one configuration field:
- Routing intent: "When the kernel needs to send a packet for IP destination X, route it through this WireGuard interface to this peer."
- Authorization policy: "When a packet arrives via this WireGuard tunnel claiming to come from IP source Y, only accept it if peer Y is authorized for source Y."
Both are expressed by the AllowedIPs setting on each peer. Here's a minimal config:
# /etc/wireguard/wg0.conf (server)
[Interface]
PrivateKey = SERVER_PRIVATE_KEY
Address = 10.7.0.1/24
ListenPort = 51820
[Peer]
# alice's laptop
PublicKey = ALICE_PUBLIC_KEY
AllowedIPs = 10.7.0.2/32
[Peer]
# bob's phone
PublicKey = BOB_PUBLIC_KEY
AllowedIPs = 10.7.0.3/32
# /etc/wireguard/wg0.conf (alice's laptop)
[Interface]
PrivateKey = ALICE_PRIVATE_KEY
Address = 10.7.0.2/24
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 10.7.0.0/24
PersistentKeepalive = 25
On the server, AllowedIPs = 10.7.0.2/32 for alice means two things at once:
- Outbound: when the kernel routes a packet destined for
10.7.0.2, send it throughwg0and encrypt it with alice's keys. - Inbound: when an encrypted packet arrives via
wg0and decrypts to a payload claiming source10.7.0.2, accept it only if it came in encrypted by alice's keys. Otherwise drop.
That second property is the security invariant: a peer cannot inject packets claiming to come from an IP not listed in its own AllowedIPs. If alice tries to spoof a packet with source 10.7.0.3, the server will decrypt it successfully (because alice's keys are valid), check the inner source IP against alice's AllowedIPs, find no match, and drop the packet. Cryptokey routing makes IP-source spoofing within a WireGuard mesh impossible without compromising the spoofed peer's keys.
The footgun: AllowedIPs on a peer config is also AllowedIPs for inbound traffic from that peer. If you set AllowedIPs = 0.0.0.0/0 on a server-side peer entry, you've told the server: "this peer is authorized to inject packets from any source IP whatsoever." That peer can now spoof traffic from your internal LAN, your company VPN range, anywhere. The misconfiguration is nearly invisible — wg show just lists 0.0.0.0/0 and you have to know what it means.
The common case where this actually happens: someone copy-pastes a client config that has AllowedIPs = 0.0.0.0/0 (the standard "route all traffic through the VPN" setting on a client) and uses it as the server-side peer entry too. On the client side, 0.0.0.0/0 means "send all my outbound traffic through this tunnel." On the server side, 0.0.0.0/0 means "this peer can claim to be any source IP." Same field, opposite implications, easy to confuse.
The other footgun: two peers with overlapping AllowedIPs are racy. WireGuard's behavior with AllowedIPs = 10.7.0.0/24 on peer A and AllowedIPs = 10.7.0.5/32 on peer B is that the more-specific match wins for outbound (longest prefix match, like normal IP routing), but for inbound the most-recent peer to register the prefix wins. If both peers are active simultaneously, traffic patterns can flip-flop unpredictably. The fix is to never overlap AllowedIPs between peers.
This collapsed routing-and-authorization model is the right answer for small deterministic deployments. For dynamic mesh networks where peers come and go, you want a control plane (Tailscale, Headscale, Netbird) to manage the coordinated AllowedIPs derivation rather than hand-editing configs. The WireGuard protocol stays minimal; the orchestration lives elsewhere.
Timers and liveness
WireGuard runs on UDP, which is connectionless. But the protocol maintains substantial state — keys, sender indexes, last-handshake time, receive counters, replay window — that gives it a feeling of being connection-oriented. The state is managed through a small set of timers, each triggered by either the passage of time or specific packet events.
The named timers, with their default values:
- REKEY_AFTER_TIME = 120 seconds. After completing a handshake, the initiator schedules a rekey. When 120 seconds elapse, send a fresh handshake initiation to derive new transport keys. The current keys remain valid until rejected by the rekey-after-messages or rejection-after-time.
- REJECT_AFTER_TIME = 180 seconds. After 180 seconds, the current session keys are considered invalid for receive. Any inbound transport packet decrypted under those keys is dropped.
- REKEY_AFTER_MESSAGES = 2^60 packets. Theoretical maximum messages per session before forced rekey. In practice no one hits this.
- REJECT_AFTER_MESSAGES = 2^64 - 2^13. Hard ceiling on packets per session.
- REKEY_TIMEOUT = 5 seconds. If a handshake doesn't complete (initiator sent initiation, no response received), retry after 5 seconds.
- KEEPALIVE_TIMEOUT = 10 seconds. After 10 seconds of receiving no inbound packets but having sent outbound, send a keepalive to verify the peer is still reachable.
- PERSISTENT_KEEPALIVE_INTERVAL = configurable (off by default; commonly 25 seconds when behind NAT). Send an empty keepalive packet at this interval regardless of traffic, to keep NAT mappings alive.
The keepalive design is interesting. Most VPNs treat keepalives as a server-side responsibility. WireGuard pushes them to the side behind NAT — the initiator (typically the client) sends PersistentKeepalive packets every 25 seconds to keep the NAT mapping fresh on whatever middleboxes are between the client and server. This is operationally correct: the responder usually has a public IP and doesn't need keepalives; the initiator typically lives behind NAT and does. The 25-second default beats most NAT timeout values (typically 30+ seconds for UDP).
The rekey design is also worth noting. Rekey is triggered by either time or message count, whichever comes first. The new handshake derives entirely fresh keys from a fresh ephemeral DH; old keys are erased after a brief grace period (during which already-in-flight traffic encrypted under the old key can still be decrypted). There is no "renegotiate" concept — every rekey is a full new handshake. This keeps the handshake state machine the only crypto state machine; there's no separate renegotiation path to audit.
The timer machinery makes the protocol behave like a connection: handshakes establish state, traffic flows over that state, the state expires gracefully and is replaced. From the outside it looks connectionless (UDP, no SYN/ACK, no FIN). From the inside it's stateful enough to give you forward secrecy, replay protection, and orderly key rotation.
Data packets and replay protection
After the handshake completes, transport packets carry the actual encrypted user traffic. The wire format:
| 0x04 | message type: TransportData (1 byte)
| reserved | reserved (3 bytes)
| receiver index | the peer's sender index from handshake (4 bytes)
| counter | monotonic packet counter (8 bytes)
| ciphertext | AEAD-encrypted IP packet + 16-byte Poly1305 tag
The counter is the AEAD nonce. ChaCha20-Poly1305 takes a 96-bit nonce; the 64-bit counter is padded with 32 zero bits. Each direction has its own send counter, which monotonically increases. Repeating a counter value within a session would catastrophically break the stream cipher (key-stream reuse), so the protocol forces rekey before the counter wraps.
Replay protection uses a sliding window receiver-side. Each peer tracks the highest-counter packet it has accepted, plus a bitmap of which counters within a window of (default 2048) packets below that maximum it has seen. A new packet is accepted if:
- Its counter is greater than the current max (the common case — advance the window).
- Its counter is within the window and the corresponding bit isn't set (out-of-order delivery — accept and set the bit).
- Otherwise reject (too old, or duplicate).
The 2048-packet window tolerates substantial reordering — far more than UDP's typical reordering on internet paths. The window is large enough that even adversarial reordering can't easily cause legitimate packets to be rejected.
The receiver-index field on each transport packet identifies which session state to use — important during rekey overlap when both old and new sessions are briefly valid simultaneously.
Optional pre-shared key layer
WireGuard's handshake supports an optional pre-shared key parameter, set per-peer via PresharedKey = <base64> in the config. If both sides have the same PSK configured for each other, it's mixed into the chaining key during the handshake. If neither side has a PSK, the protocol behaves as plain Noise_IK.
The PSK is not the primary authentication mechanism. Authentication still comes from the static-key DH operations. The PSK is a hedge against a specific failure mode: the day a sufficiently large quantum computer can break Curve25519. If that happens, all the DH outputs that authenticated past handshakes can be forged retroactively, and a passive attacker who recorded ciphertext can decrypt it.
The PSK provides post-quantum-style protection because it's symmetric — quantum computers don't break symmetric primitives in the same way. If both sides also share a PSK, the chaining key depends on that PSK as well as the DH outputs, so even with broken Curve25519, an attacker can't decrypt past sessions without the PSK.
This is a partial PQ hedge, not a full migration. For a full PQ story the WireGuard 2 protocol bump is the eventual path; until then, deploying PSKs adds belt-and-suspenders for environments where quantum-resistance matters today (long-lived archived ciphertext, sensitive government contexts, harvest-now-decrypt-later concerns from post-quantum-cryptography-in-transit).
PSK distribution is your problem, not the protocol's. Standard advice: generate the PSK once with wg genpsk, distribute it via the same secure channel you use to distribute the public keys themselves (out of band, manual, encrypted email, password manager — whatever you trust for the public key exchange).
What WireGuard does not do
The omissions are as important as the inclusions. WireGuard deliberately does not provide:
- Identity federation, RBAC, group membership. A peer is a public key; what that public key represents semantically is your problem above the protocol. For multi-user deployments, the answer is a control plane like Tailscale or Headscale.
- Certificate-based trust. No X.509, no CA, no CRL, no expiry. Public keys are forever-valid until removed from the responder's peer list.
- Algorithm negotiation. No way to pick a different cipher per session or per peer. ChaCha20-Poly1305, BLAKE2s, Curve25519, full stop.
- Multi-hop routing. A WireGuard interface terminates at one peer per
AllowedIPsentry. Cascading three WireGuard hops requires three interfaces and configuring each hop to forward to the next — seemulti-hop-wireguard-cascadefor the practical setup. - Traffic obfuscation. WireGuard packets look like WireGuard packets. The header byte is 0x01/0x02/0x03/0x04, the lengths are predictable, the timing has patterns. In censorship environments WireGuard is reliably fingerprintable; mitigations live above the protocol — wrap WireGuard in obfuscators, or use a different protocol like sing-box's REALITY (see
xray-reality-vs-wireguard). - Active congestion control on the tunnel. WireGuard is UDP — there's no tunnel-level throughput throttling. Inner TCP handles its own congestion control.
- Per-peer rate limiting. Done above the protocol if you need it (Linux tc, nftables, BPF).
- Built-in NAT traversal. WireGuard expects the initiator's
Endpointto be reachable; if both peers are behind NAT and need to find each other, you need a coordination service (a STUN-style peer-discovery server, or a control plane that does this for you). - Logging/auditing of session events. The kernel module exposes minimal information (last-handshake time, transmit/receive counters per peer). Anything more detailed is something you build with bpftrace, eBPF, or whatever telemetry tooling you bolt onto the host.
Each omission was a conscious decision. Adding any of these would expand the protocol surface, the implementation, and the configuration burden. The argument is that if you need them, you should build them at a layer where richer policy semantics are appropriate, not graft them onto the tunnel protocol.
Hands-on exercise
Read a peer config and explain each line.
Tools: text editor. Runtime: 10 minutes.
Given the example config from the cryptokey-routing section, walk through line by line and answer:
- What is the difference between
[Interface]and[Peer]? What state does each section configure? - The server has
Address = 10.7.0.1/24. What kernel-level effect does this have? (Hint: thewg-quicktool processes this; barewgdoes not.) - Why does the server's peer entry for alice have
AllowedIPs = 10.7.0.2/32while alice's peer entry for the server hasAllowedIPs = 10.7.0.0/24? - Alice's config has
PersistentKeepalive = 25. The server's config does not. Why is this asymmetric? - The server's
Endpointis omitted in alice's view of the server peer. Wait — actually, alice hasEndpoint = vpn.example.com:51820and the server doesn't have anEndpointfor alice. Why? - What happens at the kernel routing-table level when alice's
wg-quick upfinishes processing this config?
Stretch: explain why setting AllowedIPs = 0.0.0.0/0 on the server's peer entry for alice would be a security hole, and what it would actually allow alice to do.
Generate a keypair and inspect the public/private split.
wg genkey | tee privatekey | wg pubkey > publickey
cat privatekey # 32 bytes base64-encoded — never share this
cat publickey # 32 bytes base64-encoded — share this with peers
Stretch: generate a PSK with wg genpsk and explain where in the handshake state machine it would be mixed in if both peers had it configured.
Common misconceptions and traps
"WireGuard is just faster OpenVPN." It's not just faster; the policy model, handshake design, and operational philosophy are different. Cryptokey routing is a different security model than X.509-with-CRL. The fixed cryptosuite is an explicit design stance, not an oversight. The kernel implementation reflects a different architectural choice than user-space-with-DCO. Calling WireGuard "fast OpenVPN" misses what's actually different.
"AllowedIPs is just a route list." It's a route list AND an authorization policy. Setting AllowedIPs = 0.0.0.0/0 on a server-side peer entry is much worse than the same setting on a client-side peer entry, because on the server it grants spoofing authority. The same field has very different security implications depending on which side it's on.
"Because it's UDP, WireGuard has no meaningful state." It maintains transport keys per peer, sender indexes, monotonic counters, replay windows, last-handshake timestamps, rekey timers, keepalive timers, and a peer table. This is substantial state — it just isn't "connection state" in the TCP sense.
"The pre-shared key is the main authentication mechanism." It isn't. Static-key DH operations during the Noise_IK handshake provide authentication. The PSK is an optional symmetric layer mixed into the chaining key — an additional defensive measure, primarily for post-quantum hedging.
"Minimalism means no tradeoffs." The omissions push real work to higher layers. Identity federation, certificate revocation, dynamic peer discovery, traffic obfuscation, fine-grained access control — all things you'll likely need in any non-trivial deployment, none of which WireGuard provides. The protocol stays small; the operational surface above it can become substantial.
"WireGuard has no NAT issues because it's UDP." It has fewer NAT issues than IPsec, and the cookie machinery handles many edge cases gracefully, but it's not magic. Symmetric NATs, carrier-grade NATs, and changing source ports between handshake and data still cause problems. PersistentKeepalive is the standard mitigation but it's a workaround, not a structural fix. For mesh deployments where peers are all behind NAT, you need a coordination service.
"The cryptosuite will be upgraded eventually." Yes — but not via in-band negotiation. The next cryptosuite ships as WireGuard 2 with a wire-incompatible handshake. Existing deployments will run both protocols in parallel during transition. There is deliberately no version-negotiation field in WireGuard 1's handshake.
"You can use any port; 51820 is special." 51820/UDP is the IANA assignment. It isn't special protocol-wise — many deployments run on different ports, especially when 51820 is blocked. There's nothing in the protocol that requires it. The ListenPort can be any UDP port the operator chooses.
"Roaming peers reconnect automatically." They do, but the reconnection requires a handshake initiation from the new endpoint, which means the initiator sends one. If the initiator's network changes IP and it doesn't notice (because nothing was sending), the responder has the old endpoint cached and traffic stalls until the initiator sends something or PersistentKeepalive fires. The 25-second default is what makes roaming feel seamless.
Where the design lands
WireGuard's protocol design is the answer to the question "what's the smallest thing we can ship that's a real, modern VPN?" The answer is small enough to read, small enough to formally verify, small enough to run in the kernel without bloating it, and small enough that the configuration story for one tunnel between two peers fits in twenty lines.
The cost is real. Identity federation, dynamic peer management, rich access control, traffic obfuscation, multi-hop routing — none of these are protocol features. They're either things you build at higher layers (Tailscale, Headscale, Netbird, custom orchestration) or things you accept the absence of. For deployments where you control both endpoints, know all the peers, and have a mechanism to distribute static-key configs, WireGuard is the right protocol-level choice.
For deployments at any other shape — censorship environments, large dynamic fleets, federated multi-tenant access — WireGuard alone is insufficient. The protocol's minimalism is a feature; building the missing surface above it is your work.
The next module (tor-onion-routing-and-circuit-anonymity — coming soon) leaves the point-to-point tunnel model entirely and goes through Tor's circuit-based onion routing: how anonymity comes from indirection rather than encryption alone, why three hops, what the directory authorities do, and where Tor's threat model differs from a normal VPN.
Further reading
- Protocol & Cryptography — WireGuard official docs — the primary explanation of timers, packet formats, key derivation, and cookies.
- WireGuard: Next Generation Kernel Network Tunnel — NDSS 2017 — Donenfeld's design paper. The clearest statement of WireGuard's protocol philosophy.
- Formal Verification of the WireGuard Protocol — what's been mechanically checked about the handshake's security properties.
- The Noise Protocol Framework — rev34 — Trevor Perrin's Noise spec, with full pattern definitions.
- RFC 5869 (HKDF), RFC 8439 (ChaCha20-Poly1305), RFC 7748 (Curve25519) — the underlying primitives.
// related reading
OpenVPN, the friendly compromise
Why OpenVPN lasted so long: TLS in user space, TUN vs TAP, UDP vs TCP, and the flexibility costs that newer tunnels tried to remove.
sing-box and Xray architecture
How sing-box and Xray actually work: inbounds, outbounds, routing, DNS, transport modules, and why these systems are frameworks, not one protocol.
Self-hosting behind Cloudflare Tunnel without a public port
How to use Cloudflare Tunnel for published apps and private-network routes, when to use Access, and where Tunnel stops being the right tool.