What it takes to run it
Two services and a database. The master owns everything that decides anything; a node is a machine with WireGuard and an agent. Neither needs to reach the other in the direction you would expect.
The master
An ASP.NET Core service on .NET 10 with SQL Server behind it, serving the client API, the node API and the admin console on one HTTPS listener. A development run uses SQLite and creates its own database.
Each node
Linux with kernel WireGuard, or Windows Server with WireGuardNT. The agent needs the master's URL, an enrollment key, a name and its public address. Nothing else.
The clients
The Windows app and service, or the mobile app pointed at your master's address on the sign-in form. Tagged builds produce installable APKs and server archives.
Three, and only one of them inbound to the master
443 / TCP
Master. The client API and the admin console share this listener, so put a reverse-proxy rule or an IP allowlist in front of /admin.
51820 / UDP
Node. The WireGuard tunnel itself.
51821 / TCP
Node. The control API clients use to redeem a ticket and to tear a session down.
The master never connects to a node
Every node-to-master call is outbound from the node, which is what lets nodes sit behind NAT and firewalls you do not control. You do not need an inbound path to the master from anywhere except your clients.
Bring the master up
# development: SQLite, schema created from the model $ ASPNETCORE_ENVIRONMENT=Development \ dotnet run --project src/Master/iVPN.Master.Api # production: SQL Server, migrations applied deliberately $ dotnet ef database update \ --project src/Master/iVPN.Master.Infrastructure \ --startup-project src/Master/iVPN.Master.Api
The first administrator comes from configuration —
Admin:BootstrapEmail and
Admin:BootstrapPassword — applied once at startup and
ignored the moment any administrator exists. Registration only ever
produces an ordinary user, and nothing promotes one, so without this
a fresh master is inert.
Change the bootstrap password before anyone can reach it
Supply both values from an environment variable or a secret store, start once, then remove them. A value left behind cannot restore access later, because the bootstrap is skipped as soon as an administrator exists.
Four steps, in this order
Mint an enrollment key
In the console, under Enrollment keys. It is shown once — only its hash is stored, so it cannot be recovered.
Install WireGuard and the agent
On Linux that is the kernel module and the wg tools; on
Windows Server, WireGuardNT. The agent is a service.
Configure and start it
Master URL, enrollment key, node name, hostname and public address — all required, and the agent refuses to start without them rather than guessing.
$ dotnet run --project src/Node/iVPN.Node.AgentApprove it
The node enrolls on its own and appears in the console pending approval. It heartbeats, and is offered to nobody, until you approve it. That is the design, not a delay.
Behind IIS?
Then IIS terminates TLS, and the master's own client-certificate settings are dead code. The binding must negotiate the certificate in the initial handshake, the site must accept one, and Windows must trust the node CA — in that order. Getting two of the three right leaves nodes failing exactly as if nothing were configured, so the console prints the remedy rather than logging it where nobody looks.
After a certificate renewal
IIS re-registers its bindings when a site starts and drops the client-certificate policy with them — so an automated renewal breaks every node roughly every sixty days, long after anyone would connect the two. There is a script for exactly that, meant to run as a post-renewal step.
The traps are documented because they were hit
A self-hosted platform that only describes the happy path wastes your afternoon on somebody else's known problem. The repository's own notes carry the failures that cost real time here — mutual TLS that was never negotiated, a schema quietly behind the build, a key container a service account does not have — along with what the symptom looks like and where the fix goes.
- The master needs ICU: globalization-invariant mode makes SQL Server unreachable
- Auto-migration is off by default — a production schema should not be changed by whichever instance boots first
- A schema behind the build is reported at startup by name, not discovered as a 500 later
- The node CA is downloadable from the console for the trust step
Planning a deployment?
Tell us the shape — how many nodes, which platforms, what you are putting in front of the master — and we will tell you what to watch for.