Headscale: The Open-Source Tailscale Control Server That Puts Your Mesh VPN Back in Your Hands

Headscale: The Open-Source Tailscale Control Server That Puts Your Mesh VPN Back in Your Hands

Headscale: The Open-Source Tailscale Control Server That Puts Your Mesh VPN Back in Your Hands

"Tailscale open-sourced the clients and kept the coordinator. Headscale is the project that quietly took the last piece back."
Tailscale is one of the nicest pieces of networking software ever shipped: install a client, and suddenly every machine you own can reach every other machine by name, over WireGuard, through NAT, with almost no configuration. But there has always been an asterisk. The clients are open source; the control server — the coordinator that exchanges WireGuard public keys, hands out IP addresses, and decides which node may talk to which — runs as a hosted service in someone else's account. Headscale (GitHub: juanfont/headscale) removes that asterisk. It is an open-source, self-hosted implementation of the Tailscale control server, and as of August 2026 it carries roughly 41,000 stars, is written in Go, ships under BSD-3-Clause, and is maintained by Kristoffer Dalby and Juan Font with about 251 contributors. For a blog about software you run on your own hardware, it is the missing layer: a private mesh network whose brain is also yours. This is the honest breakdown — what it does, how the protocol actually works, what it really costs, where your network metadata lands, and the limitations (including one genuinely surprising deployment caveat) that decide whether it belongs in your homelab. Headscale acting as the control plane for a private WireGuard mesh

1. What Headscale Is (and Isn't)

Headscale is not a VPN client and not a replacement for WireGuard. It is a control server. In Tailscale's architecture there are two distinct roles:
  • The data plane — the actual encrypted packets flowing directly between your devices over WireGuard. This is peer-to-peer whenever possible and never needs the coordinator.
  • The control plane — the coordinator. It authenticates nodes, exchanges their WireGuard public keys, assigns each node an IP in the 100.x range, computes and pushes the network map (who you're allowed to reach), and hands out DERP relay assignments for the cases where direct connection fails.
Tailscale the company runs the control plane as a hosted service. Headscale reimplements it as a single Go binary you run yourself. Your devices still run the standard Tailscale client (tailscale up --login-server https://your-headscale), pointed at your box instead of Tailscale's. What it isn't is a multi-tenant product. The project states its scope deliberately: "a single Tailscale network (tailnet), suitable for personal use, or a small open-source organisation." If you were hoping to host isolated networks for fifty customers from one Headscale instance, that is explicitly not the design. It does one tailnet, and it does it well.

2. How the Protocol Actually Works

Under the hood, Headscale speaks the same /ts2021 protocol the official clients expect. The mechanics: 1. A node starts the Tailscale client and authenticates against your Headscale URL — typically with a pre-auth key you generated, or through OIDC if you've wired up an identity provider. 2. Authentication rides on a Noise protocol implementation, and control traffic flows over WebSockets. 3. Once registered, the node's WireGuard public key and chosen IP land in Headscale's database. 4. Every node long-polls the control server, which pushes back a network map: every peer it's allowed to see, their keys, their IPs, allowed routes, and DERP assignments. 5. With that map in hand, nodes attempt direct UDP hole-punching to each other. If that fails (symmetric NAT, restrictive firewalls), traffic falls back to a DERP relay — Tailscale's encrypted fallback relay. This is the part most people forget: Headscale replaces the coordinator, but DERP relays are still needed unless you also self-host them. Node registration, key exchange, and network map distribution The tech stack is sober and production-minded: GORM over PostgreSQL or SQLite (with a pure-Go, cgo-free SQLite driver so static binaries stay portable), go-gormigrate for migrations, Tailscale's own squibble for schema integrity, go-chi for routing, huma for an OpenAPI-described REST API, cobra/viper for the CLI, and zerolog plus Prometheus metrics for observability. The presence of go-deadlock in the dependency tree is a tell: this is concurrency-heavy code where the maintainers have been burned by locking bugs before — and the v0.29.2 changelog shows them fixing exactly that class of issue.

3. Why "Self-Hosted Control Plane" Matters

The privacy argument here is subtler than "my data stays home," and it is worth being precise, because the naive version of this argument is wrong. Your traffic was always yours. Even with commercial Tailscale, your actual packets are end-to-end encrypted WireGuard between your own devices. Headscale does not make your data more private in transit, because it was already private. What Headscale takes back is the metadata. The coordinator knows the shape of your network: which devices exist, their names and OS versions, their IPs, which machines are allowed to talk to which, when each node was last seen, and the ACL policy that governs everything. That is a genuinely sensitive map — it is, in effect, a topology diagram of your digital life. Running Headscale means that map lives in a database on your disk instead of in a vendor's account. It also removes a dependency. If the hosted control plane has an outage, or changes its pricing, or decides your use violates its terms, your mesh keeps running on the data plane but you lose the ability to add nodes or change policy. Self-hosting means your network's brain has no third-party kill switch. The honest asterisk: DERP. Unless you run your own DERP servers, the fallback relay path still touches Tailscale-operated infrastructure — though it carries only encrypted traffic that the relay cannot read. You can self-host DERP for full sovereignty; many homelabbers simply accept the default and encrypt the rest.

4. Getting Started (and the Deployment Caveat Nobody Expects)

The standard flow is: install Headscale from a release binary or package, write a config.yaml, create a namespace, generate a pre-auth key, and register nodes: ``bash

create a namespace (a logical group of nodes)

headscale namespaces create mynet

generate a pre-auth key

headscale preauthkeys create --namespace mynet --reusable --expiration 24h

on each client, point at YOUR control server

tailscale up --login-server https://hs.example.com --authkey <KEY>
` Now the genuinely surprising part, and it is worth its own paragraph because it trips up everyone who assumes "it's Go, so it's a container": the maintainers actively discourage running Headscale in Docker. Container images exist, but the project's position is that reverse-proxying and containerized networking add failure modes to a service whose whole job is delicate NAT traversal. The recommended path is running the binary directly on the host, with TLS terminated properly in front of it. If your reflex is "I'll just throw it in my compose file," read the docs first — this is the single most common self-inflicted wound in the Headscale community.

5. The Cost, Honestly

Software: $0. BSD-3-Clause is about as permissive as it gets — use it, modify it, ship it, just keep the notice. A public endpoint with TLS. This is the real requirement and it is not negotiable: every client needs to reach your control server over HTTPS on a stable, publicly resolvable hostname. A small VPS runs $4–$10/month. If you already have a server with a public IP and a domain, it is effectively free. DERP (optional). If you want full sovereignty you can run your own DERP relays — more VPS cost, more maintenance. Most users skip this and accept encrypted fallback. Your time. Moderate. Expect an afternoon for a first working tailnet, and a bit of ongoing care: Headscale is still v0.x (v0.29.2 as of July 2026), so read the changelog before you upgrade. The project ships frequent patch releases, which is a good sign for responsiveness and a reminder that the API can still move. Against Tailscale's hosted tiers, the calculus is not purely financial — the free tier is generous for individuals. The reason to self-host is control of the metadata and independence from a vendor, not the monthly fee. Be honest with yourself about which one you actually want.

6. Honest Limitations

A deep-dive that only quotes the README is marketing. Here is the fine print:
  • Single tailnet by design. No multi-tenancy. Wrong tool if you need isolated networks per customer or per team.
  • Still pre-1.0. v0.29.x means the API and config can change between releases. Upgrade deliberately, with a backup.
  • Docker is officially discouraged. Plan to run the binary on a host, not in your container stack.
  • Security posture is mixed. OpenSSF Scorecard rates it around 5.3/10: excellent on maintenance (10/10) and code review (10/10), but 0/10 on Token-Permissions, Security-Policy, Fuzzing, and Signed-Releases. It has no published security policy and does not sign release artifacts. For a security-adjacent piece of infrastructure, that is worth knowing before you expose it to the internet.
  • DERP remains a third-party dependency unless you self-host relays.
  • You are the SRE. Node registration, key rotation, database backups, and uptime are yours. When Headscale is down, you cannot onboard new nodes — plan accordingly.
  • It is not a Tailscale competitor feature-for-feature. Features like MagicDNS, ACLs, and exit nodes work, but the hosted product's polish and extras (Funnel, Serve, some enterprise controls) are not all present.
None of these are disqualifying for the target user. They are the difference between choosing a tool and being surprised by one.

7. Headscale vs the Alternatives

| | Headscale | Tailscale (hosted) | NetBird | Raw WireGuard | |---|---|---|---|---| | Control plane | self-hosted | vendor SaaS | self-host or cloud | none (manual) | | License | BSD-3-Clause | proprietary server | Apache-2.0 | GPL-2.0 | | Multi-tenant | ❌ single tailnet | ✅ | ✅ | N/A | | Setup effort | moderate | trivial | low | high | | Metadata location | your DB | vendor | your infra (self-host) | N/A | | NAT traversal | ✅ (+ DERP) | ✅ | ✅ | manual | The short version: pick Headscale if you want the Tailscale experience with the control plane on your own box and you're comfortable running a service. Pick hosted Tailscale if you want zero maintenance. Consider NetBird if you want a more batteries-included self-hostable alternative with multi-tenancy; pick raw WireGuard only if you enjoy hand-maintaining key files and don't need NAT traversal.

8. Who Should Run It

Run it if: you already run a server with a public IP, you care that a vendor holds a topology map of your network, you want to learn how overlay networking actually works, or you want your mesh to keep working even if a control-plane vendor changes its mind about you. Skip it if: you want a five-minute setup and never want to think about it again (use hosted Tailscale), you need true multi-tenancy (NetBird), or you're not prepared to keep a public-facing service patched and backed up. A control server you forget to maintain is worse than a hosted one, because it fails silently. For this blog's readers — the ones already running Home Assistant, Uptime Kuma, RustDesk, Immich, and a Docker host on their own hardware — Headscale is the connective tissue. It is what turns "a bunch of services I own" into "a private network I own," where reaching your home lab from a café is as simple as typing a hostname, and where the map of that network never leaves your disk.

9. A Real Deployment Walkthrough

Abstract architecture is easy; getting a first node joined is where people stall. Here is the shape of a working setup on a small VPS: Step 1 — a public hostname with TLS. Clients must reach Headscale over HTTPS on a name that resolves publicly. Point
hs.example.com at your VPS and terminate TLS with Caddy or Nginx in front of the binary. Skip this and no client will ever authenticate. Step 2 — minimal config. Set your server_url to that HTTPS address, choose SQLite for a small deployment (PostgreSQL for anything bigger), set an IP prefix, and enable MagicDNS if you want hostname resolution between nodes. Step 3 — a namespace and a pre-auth key. Namespaces group nodes; pre-auth keys let machines register without an interactive browser login: `bash headscale namespaces create homelab headscale preauthkeys create --namespace homelab --reusable --expiration 24h ` Step 4 — join a node. On each machine, install the standard Tailscale client and point it at you: `bash tailscale up --login-server https://hs.example.com --authkey <KEY> ` Step 5 — verify, then expand. Run tailscale status; you should see peers with their 100.x addresses and a "direct" connection where NAT allows. Add the rest of your machines, then write an ACL policy that says what each node may reach. For anything beyond a couple of nodes, wire authentication to OIDC rather than passing pre-auth keys around — it centralizes identity and lets you revoke a single user cleanly.

10. Troubleshooting & Operational Gotchas

The failures that actually show up, in rough order of frequency:
  • Clients can't reach the control server. Almost always TLS or DNS. Verify https://hs.example.com/health returns from outside your network, not just from the host.
  • "Direct" connections show as relayed. NAT traversal failed. Usually a symmetric NAT or a firewall blocking UDP. Ensure UDP 41641 (or your configured port) is open; you'll still work via DERP, just slower.
  • Node registered but can't see peers. ACL policy. A default-deny policy with no rules grants nothing — check that your policy actually permits the traffic you expect.
  • Things broke right after an upgrade. You're on v0.x. Read the changelog, back up the database first, and don't auto-upgrade a control plane you depend on.
  • Connection storm after a policy change. This exact class of bug (map generation serializing on a lock) was fixed in v0.29.2; if you're on an older build, upgrade.
  • Docker networking weirdness. Re-read section 4. The maintainers discourage containers for a reason, and "it works on my compose file" is a common source of subtle breakage.
Most of these resolve in minutes once you internalize the split: control plane (must be reachable over HTTPS) versus data plane (peer-to-peer, wants UDP). Debugging gets dramatically easier when you know which half is broken.

11. Access Control: The ACL Model, Plainly

Once you have more than a couple of nodes, the ACL policy is the part that actually matters — it decides what may talk to what. Headscale uses Tailscale's policy format, written in HuJSON:
`json { "acls": [ { "action": "accept", "src": ["tag:server"], "dst": ["tag:homelab:"] }, { "action": "accept", "src": ["group:admins"], "dst": [":"] } ], "tagOwners": { "tag:server": ["group:admins"] } } ` Three ideas carry most of the weight. Users and groups identify people (especially useful once you've wired OIDC). Tags identify machines by role rather than by owner — a server doesn't belong to a human, so tag it. Rules connect sources to destinations, and the default is deny: if no rule permits it, the connection doesn't happen. The practical advice: tag your servers, keep human devices in a group, and write the narrowest rule that does the job. It's tempting to write {"action":"accept","src":[""],"dst":[":"]}` just to make things work — and that does work, but you've then built a flat network where any compromised node reaches every other node. The whole point of owning the control plane is being able to express real boundaries, so take the twenty minutes to write them.

12. Where Headscale Fits in a Self-Hosted Stack

The reason Headscale is worth the setup effort is not the VPN itself — it's what the VPN lets you stop doing. Every service in this blog's orbit (Home Assistant, Uptime Kuma, Frigate, Immich, RustDesk) needs some remote-access story, and the default answers are all slightly bad: open a port and hope, pay for a tunnel subscription, or expose a reverse proxy to the open internet with your security riding on one app's login page. Headscale collapses that problem. Put the services on a tailnet and they're simply reachable by name from your laptop and your phone, encrypted, unroutable from the public internet, with access governed by one ACL file you control. No per-service port forwarding, no monthly tunnel fee, no public attack surface on each individual app. The compounding effect is real: your Home Assistant is reachable without Nabu Casa, your Frigate feeds stream to your phone without exposing port 5000, your changedetection instance is private by construction, and your RustDesk relay sits behind the same private network. One piece of infrastructure replaces four workarounds — and because it's yours, it doesn't change pricing, deprecate a tier, or shut down. That is the actual argument for self-hosting the control plane: not the monthly fee, but the fact that your network's front door belongs to you.

Related

Comments (0)

No comments yet. Be the first to comment!

Leave a Comment