Headscale VPN

To help my local radio club remotely control some of the rigs at the clubhouse, I’ve deployed a headscale control server to enable location-agnostic private mesh networking between different sites, services, and users while leveraging the very nice Tailscale clients and technology developed commercially.

Location Agnostic

The servers providing access to the remote radios can be located anywhere, and can move at will. This allows the deployment to be nimble against different ISPs and network topologies.

Where possible, direct peer-to-peer tunnels will be established, otherwise different tunneling protocols are used to achieve NAT traversal or relaying.

Private Mesh Networking

The headscale model presents a single “tailnet” or network namespace, although ACL policies can be set on the control server to affect traffic routing between tagged nodes.

Deploying the Server

My favorite deployment method these days is docker compose and sqlite because it often just works and nothing further is needed.

docker compose up


version: '3.4'
    image: headscale/headscale:latest
      - ./config:/etc/headscale/
      - ./data:/var/lib/headscale
      - 27896:8080
    command: headscale serve
    restart: unless-stopped


(short version, see the example config for full commentary)

server_url: https://headscale.w7dg.net

private_key_path: /var/lib/headscale/private.key
  private_key_path: /var/lib/headscale/noise_private.key
  - fd7a:115c:a1e0::/48
# SQLite config
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
  override_local_dns: true
  magic_dns: true
  base_domain: w7dg.net


server {
    listen ssl;
    listen [::]:443 ssl;
    server_name headscale.w7dg.net;
    ssl_certificate /etc/letsencrypt/live/w7dg.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/w7dg.net/privkey.pem;

    location / {
        proxy_pass http://localhost:27896;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection Upgrade;
    access_log  /var/log/nginx/headscale.access.log  main;
    error_log /var/log/nginx/headscale.error.log warn;

(ya know, thankfully already had a nice webserver running and SSL certs 😀)

Create a User

Users control nodes in the tailnet. There is no authentication in headscale by default, users are merely containers for nodes and are used to apply policies. The admin decides who a node who it will belong to when registering and can reassign nodes at will.

docker compose exec headscale headscale user create foo

Connecting Clients

  • On windows, click Start, search for “command prompt”, right-click and select “Run as Administrator”.
  • On linux, use sudo to run as root.
tailscale up --login-server=https://headscale.w7dg.net

If it worked, this spits out a nodekey that the user sends to the admin to register the node / client on the server. If a tailscale login page is seen, then something is wrong.

Adding a node

docker compose exec headscale headscale node register --user foo --key nodekey:......

Subnet Routing

From a client that wants to share a subnet

sudo tailscale up --advertise-routes= --login-server=https://headscale.w7dg.net

Enable the Routes

docker compose exec headscale headscale route list
docker compose exec headscale headscale route enable -r <id>

Accept the Advertisements

From the clients that should connect to the subnets:

tailscale up --accept-routes ...

Ease of Use

Hopefully, for a network like ours, onboarding user devices will be relatively infrequent.

However, getting a node key in email, and running a command on a server is something at least 3 people in the club could easily do. With the possibilities for easily connecting sites with cellular internet that are currently unroutable, I’m looking forward to digging deeper and unpacking the ACL policy, so we can deploy this into “production”.

Next Steps

  • Configuring DERP / STUN infrastructure so that clients unable to connect directly can connect through our infrastructure without leeching from tailscale.
  • Configuring ACL and testing the whole thing end to end


Written on February 4, 2023

Please be patient, submitting a comment may take up to 30 seconds