Home / Deploy
Self-hosting

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.

Ports

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.

First run

Bring the master up

master
# 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.

Adding a node

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.

node
$ dotnet run --project src/Node/iVPN.Node.Agent

Approve 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.

Operational reality

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.