Skip to content

VPN Strategy

The VPN provides secure access to internal infrastructure for SSH and administrative tasks. It's separate from Cloudflare Tunnel, which handles web traffic - the VPN is specifically for terminal/SSH access.

Current Setup: Headscale

Tailscale is a mesh VPN built on WireGuard. It's incredibly easy to use - install the client, log in, and you're connected to the mesh. The catch is that Tailscale's coordination server (which handles authentication and key exchange) is a hosted service, and OIDC integration costs $18/user/month.

Headscale is an open-source implementation of that coordination server. It speaks the same protocol, so standard Tailscale clients work with it. We run Headscale ourselves, which means OIDC integration is free and we can have unlimited users.

Component Details
Coordinator Headscale (headscale.minnova.io)
Client Tailscale (standard client on any OS)
Auth Authentik OIDC
Access Bastion server (100.64.0.6)

Headscale runs on its own server with a public IP. This is required because Tailscale clients need to reach it from anywhere to join the VPN. Traefik sits in front of it handling TLS with Let's Encrypt certificates.

Why Headscale over Tailscale SaaS

The main driver is cost. Tailscale's free tier supports 3 users with basic auth. To use OIDC (required for proper SSO with Authentik), you need their business plan at $18/user/month. For a small team that adds up fast.

With Headscale:

  • OIDC integration is free (it's just a config option)
  • Unlimited users
  • We use standard Tailscale clients (same great UX)
  • Full control over the coordination server and ACLs
  • No vendor dependency for critical infrastructure

The tradeoff is that we're responsible for running it. If Headscale goes down, no one can get VPN access. That's why it runs on a dedicated server with minimal other services.

Alternatives Considered

Tailscale SaaS: Great product, but $18/user/month for OIDC adds up. The free tier (3 users, basic auth) isn't enough for a team that wants proper SSO.

Cloudflare Zero Trust: Free for up to 50 users and includes OIDC. Good for web app access (they have browser-based SSH too). But for raw TCP/SSH access, a proper VPN is more flexible. We use both - Cloudflare for web, Headscale for SSH.

Netbird: Another open-source mesh VPN with free OIDC. Newer than Headscale with a smaller community. Might be worth revisiting later, but Headscale has more production mileage.

Access Flow

Here's what happens when someone connects:

# 1. Run this on your machine
tailscale login --login-server https://headscale.minnova.io

# 2. Browser opens to Authentik
#    Log in with your credentials + MFA

# 3. Once authenticated, you're on the VPN
#    Your machine gets a 100.64.x.x address

# 4. SSH to bastion, then to internal servers
ssh user@100.64.0.6       # bastion (via VPN)
ssh 10.0.2.1              # from bastion to apps server

The key insight is that the VPN gives you access to the bastion (100.64.0.6), and from there you can reach internal servers on their private IPs (10.0.x.x). You can't SSH directly to 10.0.2.1 from your machine - you hop through the bastion.

VPN vs Cloudflare Tunnel

We use two different access methods for different purposes:

Cloudflare Tunnel for web applications (Atlantis, Grafana, Authentik). These are browser-based, HTTPS-only. The tunnel handles TLS and DDoS protection. Easy for anyone to access without installing software.

Headscale/Tailscale for SSH access. VPN gives raw TCP connectivity, so you can SSH, run commands, tunnel ports. More powerful but requires the Tailscale client installed.

The separation also means if Cloudflare has an issue, SSH still works (and vice versa). Headscale can't use Cloudflare Tunnel because Tailscale's protocol requires WebSocket POST, which tunnels don't support.

Adding a New User

  1. Create their account in Authentik
  2. Add them to the appropriate group (founders, contractors, etc.)
  3. Have them install Tailscale on their machine
  4. They run tailscale login --login-server https://headscale.minnova.io
  5. Browser opens, they authenticate with Authentik
  6. They're on the VPN and can SSH to bastion

What they can access depends on Headscale ACLs (infra/ansible/files/headscale-acl.json), which reference Authentik groups. Different groups can have different access levels.