Why we're tearing out a VPN that works
The old OpenVPN setup wasn't broken. It was a pile of per-user certificates and a manual onboarding ritual. Here's the case for replacing it with a self-hosted WireGuard mesh tied to the directory, and the one rule that keeps the swap from becoming an outage.
A note on this series: this is real client work, anonymized. The company, hostnames, IPs, and domains are stand-ins (
example.com,10.0.0.x). The architecture, decisions, and lessons are exactly as they happened.
A client’s remote-access VPN wasn’t broken. People connected, they reached internal systems, it had been fine for years. So why rip it out? Because “it works” and “it’s worth keeping” are two different claims. The way this one worked charged a small tax on every new hire and every person who left, and that tax was quietly growing.
This series is the build log for swapping a classic OpenVPN setup for a self-hosted WireGuard mesh (NetBird) with single sign-on in front of it. Part 1 is the part worth getting right before touching anything: the reasoning, and the one discipline that keeps a migration like this from turning into a bad day.
The mental model: identity belongs in the directory, not in a pile of certs¶
Here’s the old world. OpenVPN authenticates each user with a client certificate, a .ovpn
file with an embedded key. Onboarding meant SSHing to the VPN box, running the EasyRSA
“new client” command, generating a .ovpn, and handing it over. Offboarding meant remembering
to revoke that cert.
Why a certificate per user quietly rots
Each certificate is a long-lived secret living in someone’s Downloads folder and synced to who knows where. Nothing asks for a second factor, so holding the file is the authentication. And access isn’t tied to whether the person still works here. It’s tied to whether their cert is still valid. The real answer to “who can connect?” drifts out of the directory and into a scatter of files. None of that is an emergency today. All of it is debt.
The new world flips it around so the directory is the source of truth. The client already runs an identity system, FreeIPA, that knows who works there and which teams they’re on. Access should hang off of that, with a second factor required, granted and revoked in one place. The VPN client’s only job is to ask “who are you?” and trust the answer the identity provider gives back. When someone loses a laptop, leaves, or moves teams, you change the directory and access follows. There’s no file to chase down.
What we’re building¶
remote user (laptop / phone)
│ WireGuard, authenticated by SSO + MFA
▼
┌─────────────────────────┐ ┌──────────────────────────┐
│ gateway VM (DMZ+LAN) │ │ identity VM (LAN) │
│ Caddy (TLS, DNS-01) │ OIDC │ Keycloak ── LDAPS ──▶ │
│ NetBird mgmt/signal │◀──────▶│ (federates the directory)│
│ coturn relay │ │ Postgres │
└────────────┬────────────┘ └─────────────┬────────────┘
│ advertises internal routes │ read-only bind
▼ ▼
internal network 10.0.0.0/24 FreeIPA directory
Three moving parts, each with one job:
- Keycloak, on a LAN-only identity VM, federates the existing directory read-only and becomes the single sign-on front door where the second factor is enforced.
- NetBird, self-hosted on a gateway VM, is the WireGuard control plane. It checks each peer against Keycloak and wires up the mesh.
- Caddy handles TLS for all of it and gets its certs automatically over DNS-01.
The old OpenVPN box doesn’t get touched yet. That isn’t an oversight. It’s the whole strategy.
The one rule: build alongside, never on top¶
Never cut over by deletion
The mesh goes up next to the working VPN, not in place of it. OpenVPN keeps running on its
own port the whole time. Users move over in waves, and a wave only stops using OpenVPN once
it’s confirmed working on the mesh. At any point the new stack can come down
(docker compose down, drop one firewall rule) without touching the thing people rely on
today. The VPN stays as the fallback until nobody needs it, and even then it sticks around
for site-to-site backups.
There’s a corollary I committed to early: the proof of concept is production. No throwaway test domains, no “we’ll redo it properly later.” Real DNS names, real certificates, real identity federation from day one. The gap between a POC and the real thing is exactly where migrations tend to die, so I’d rather not have one. Test like you fly.
Honest limits, and what I’m deliberately leaving for later¶
- OpenVPN isn’t going away. It keeps carrying a site-to-site backup tunnel indefinitely. This project only moves the interactive road-warrior users.
- MFA is staged on purpose. I start with Keycloak’s own MFA (passkeys or TOTP) because it’s fully under my control and lets the build move. Handing MFA authority to the client’s cloud IdP (Entra Conditional Access) is a later phase, so the whole project doesn’t stall on a licensing conversation.
- One weak spot I can’t fix here. A remote office on the far side of the world sees about 250 ms of round-trip latency. That’s geography, and swapping the VPN won’t change it. The real fix is regional compute, tracked separately. I’m not going to pretend the mesh speeds up physics.
What’s next¶
The decision is made and the architecture is on paper. Part 2 builds the identity spine: standing up Keycloak on its own VM and federating the existing directory over LDAPS, read-only, so the mesh can authenticate people against the system that already knows them.