Mint a one-time enrollment key
From the console, with a label, a use count, an expiry and — if you want the node live the moment it appears — pre-authorization. Keys are stored hashed and shown once.
Enroll
The agent generates a WireGuard keypair and a TLS keypair, neither of which ever leaves the machine, and sends a certificate signing request with the enrollment key.
Issue a certificate and a subnet
The key is validated and spent. The node is created in
pending approval, issued a certificate from the master's
own CA, and carved a tunnel subnet — a /24 out of the
global pool by default, which is 253 clients on that node.
Approve it
Until you do, the node heartbeats happily and is offered to nobody. Approval materializes its address pool and puts it in the server list.
Mutual TLS from then on
Every call the node makes is authenticated by that client certificate, and the node's identity comes from the certificate alone. Nothing anywhere reads a node id out of a request body, so a compromised node cannot act as a different one.
Your own certificate authority, on purpose
Nodes are numerous, often addressed by bare IP, and sometimes short-lived. Getting a public certificate for each one means public DNS for each one, and every issued certificate lands in a Certificate Transparency log — which publishes your node list to anybody who cares to look.
So the master generates a CA on first start and issues every node's certificate itself. Clients pin that CA. They already trust the master implicitly — it is where they authenticate — and extending that trust to certificates it signed is coherent.
Why not the machine trust store
Because putting the node CA there would mean every public authority in that store is also trusted to mint a node. The clients trust this one root and nothing else for node connections.
Four states, and one of them is the useful one
Enrolled, inert
It has a certificate and a subnet. It is offered to nobody until an administrator says so.
In the rotation
Offered to clients whose account may use it, subject to capacity and a live heartbeat.
Emptying
Keeps serving the sessions it has, hidden from every server list. This is how you take a node out for maintenance without cutting anybody off.
Gone now
Sessions are revoked immediately rather than allowed to finish. Choosing this is choosing the abrupt option.
Offline is deliberately not a stored state. It is derived from the last heartbeat — three missed twenty-second beats and the node stops being offered — so no background job has to race heartbeats to keep a column truthful.
Linux
Kernel WireGuard, driven through wg and
ip, with forwarding and NAT configured by the
agent. Peer churn is a few connections a minute, so the process
spawns cost nothing and the failures stay legible in the log.
Windows Server
WireGuardNT through the driver's adapter API, not the file-configured tunnel service — because reconfiguring by file means restarting the tunnel, which would drop every other client on the node each time one connects.
Two implementations, one interface
The agent, the session manager and the reconcile loop never learn
which platform is underneath. IDataPlane names nothing
WireGuard-specific either, which is what leaves room for a
TCP-capable transport later without touching the master, the
contracts or the clients.
- A peer is installed with exactly one
/32of AllowedIPs — anything wider would let one client source-spoof every other tunnel address on that node - The agent reconciles on restart: read the live peers, ask the master which sessions it should have, converge
- Usage cursors are persisted, so an agent restart never re-reports a session's history
The master going away is a designed-for case
Nodes sit behind NAT and firewalls nobody controls, so the master never connects to a node. Every ten seconds the node sends the list of sessions it believes are live, and the master replies with the set that must die. That is the reconciliation point, and it is what makes an admin's force-disconnect land within ten seconds.
Master unreachable
New sessions fail closed. No exceptions — a ticket cannot be redeemed without the master. Established tunnels keep working for a configurable grace period, then are torn down. Set the grace to zero if that is the trade you want.
Node goes quiet
The heartbeat lapses, the master hides it and revokes its sessions, and clients are offered somewhere else.
Node up, interface down
The heartbeat says so, and the master treats it as offline for listing even though the agent is alive.
Address pool exhausted
Ticket issue fails with a distinct code and the node drops out of listings until leases free up.
Client vanishes
No WireGuard handshake within the idle timeout and the node closes the session locally, then reports it.
Add a node in an afternoon
A key from the console, one service on the machine, and one approval. The deployment guide has the exact commands.