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.
Pangolin: See dedicated section below - this is a promising option that could consolidate both VPN and tunnel infrastructure.
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¶
- Create their account in Authentik
- Add them to the appropriate group (founders, contractors, etc.)
- Have them install Tailscale on their machine
- They run
tailscale login --login-server https://headscale.minnova.io - Browser opens, they authenticate with Authentik
- 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.
Future Option: Pangolin¶
Pangolin is an identity-aware remote access platform built on WireGuard that could potentially replace both Cloudflare Tunnel and Headscale with a single self-hosted solution.
What Pangolin Does¶
Pangolin combines reverse proxy and VPN capabilities into one platform:
- Web applications: Browser-based access with automatic SSL, routing, and load balancing (like Cloudflare Tunnel)
- TCP/UDP resources: Client-based access to SSH, RDP, databases, game servers, and entire network ranges (like Tailscale)
- Site-to-site: Connect isolated networks through encrypted WireGuard tunnels
- Zero-trust security: Granular access control per resource, not entire networks
The key component is Newt, a user-space WireGuard client that handles tunneling without complex WireGuard/NAT configuration.
Pangolin vs Current Setup¶
| Feature | Current (Cloudflare + Headscale) | Pangolin |
|---|---|---|
| Web apps (HTTPS) | Cloudflare Tunnel | ✅ Built-in |
| SSH/RDP/TCP/UDP | Headscale/Tailscale | ✅ Client resources |
| Site-to-site | Headscale | ✅ Newt connectors |
| NAT traversal | Both | ✅ Intelligent NAT punch |
| Self-hosted | Headscale only | ✅ Fully |
| Single control plane | ❌ Two systems | ✅ One dashboard |
| Architecture | Mesh (Headscale) + Hub (CF) | Hub-and-spoke |
| Auth | Authentik (both) | OIDC supported |
Why Consider Pangolin¶
Consolidation: Currently we run Cloudflare Tunnel for web and Headscale for SSH. Pangolin can do both, reducing operational overhead.
No Cloudflare dependency: Cloudflare's ToS restricts video streaming through tunnels (affects media servers). Self-hosted Pangolin has no such restrictions.
Granular port control: TCP/UDP resources support allow all, block all, or specific port ranges - more flexible than Cloudflare's HTTPS-only tunnels.
Cost: Free self-hosted (AGPL-3 Community Edition). Enterprise Edition is free for companies under $100K revenue.
Trade-offs¶
Hub-and-spoke vs Mesh: Pangolin routes all traffic through your VPS (hub-and-spoke). Headscale/Tailscale can do peer-to-peer mesh where devices connect directly. For our setup this doesn't matter much since the Hetzner VPS is the central point anyway.
Maturity: Pangolin is newer (YC25 company). Headscale has more production mileage. Worth monitoring stability before migrating critical infrastructure.
Browser-based SSH: Cloudflare has browser-based SSH which is convenient for quick access without VPN client. Pangolin focuses on client-based access.
Migration Path¶
If we decide to adopt Pangolin:
- Deploy Pangolin on a VPS (can coexist with current setup)
- Migrate web apps from Cloudflare Tunnel to Pangolin HTTPS resources
- Configure TCP resources for SSH access (replacing Headscale use case)
- Test thoroughly before decommissioning Headscale
- Update Authentik OIDC to point to Pangolin
Resources¶
- GitHub: fosrl/pangolin
- Newt client: fosrl/newt
- Docs: docs.pangolin.net