Deployment guide

Headscale

Overview

Headscale is an open-source, self-hosted implementation of the Tailscale control server (the coordination server, sometimes called the control plane). It lets you run your own private mesh network. Machines register with the control server using pre-auth keys or OIDC, receive an address from the 100.64.0.0/10 range, and connect directly to each other over an encrypted, WireGuard-based overlay.

This SecureAnyCloud image runs Headscale on Ubuntu 24.04 LTS on Azure. It installs the latest stable Headscale release, resolved at build time from the official GitHub releases and baked reproducibly into the image. The exact version and checksum are recorded in /opt/headscale/VERSION.

Headscale runs as a single Go binary under systemd, as the unprivileged headscale system user. It binds to loopback only (127.0.0.1:8080). nginx is the only network-facing surface. It terminates TLS on port 443 and reverse proxies to Headscale with the settings the Tailscale control protocol needs: HTTP/1.1, unbuffered long-poll streaming and long timeouts.

HTTPS is required. Tailscale clients connect to a control server only over TLS. On the first boot, this image regenerates a self-signed TLS certificate for this specific VM. The certificate Subject Alternative Names include the VM public IP and hostname.

First boot also sets the control server URL to https://<your public ip> and generates a per-instance administrative API key for the Headscale REST API. Anonymous requests to the REST API return HTTP 401, and only the per-instance API key is accepted. The key is written to /root/headscale-credentials.txt (mode 0600, root only). No shared secret and no default login ships inside the image.

What you get:

  • The latest stable Headscale release (single Go binary), run under systemd as the unprivileged headscale system user
  • The Headscale control API and REST API bound to loopback only (127.0.0.1:8080), never directly exposed to the network
  • nginx terminating TLS on :443 and reverse proxying the control protocol and REST API, tuned for the Tailscale long-poll map stream (unbuffered, 900-second timeouts)
  • Port :80 returns a 301 redirect to HTTPS for every path except an unauthenticated /healthz endpoint (HTTP 200) for load balancer probes
  • A per-instance API key generated on first boot and stored in /root/headscale-credentials.txt (0600), with no shared secret and no default login
  • A self-signed TLS certificate regenerated per VM on first boot (SAN includes the VM public IP and hostname). Replace it with your own CA-signed certificate for production
  • MagicDNS, an embedded SQLite state store, and sane defaults for IP allocation from the Tailscale 100.64.0.0/10 and fd7a:115c:a1e0::/48 ranges
  • Ubuntu 24.04 LTS base with the latest security patches applied at build time
  • Azure Linux Agent for cloud integration and SSH key injection
  • 24/7 SecureAnyCloud support with a guaranteed 24-hour response SLA

Prerequisites

  • An active Azure subscription, an SSH public key, and a virtual network and subnet in the target region
  • A subscription to the Headscale listing on Azure Marketplace
  • One or more machines running the Tailscale client that you want to join to your network

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a control server that coordinates dozens of nodes. Larger fleets can use Standard_D2s_v5 or larger.

The control server relays only coordination traffic, not your data plane. Nodes connect directly to each other.

1

Deploy from the Azure Portal

Search the Marketplace for Headscale, select the SecureAnyCloud publisher, and select Create.

Add these network security group (NSG) rules, allowing the networks your Tailscale clients connect from:

  • TCP 22 for administration
  • TCP 443 for the control server over HTTPS
  • TCP 80 for the HTTP to HTTPS redirect and the health probe
2

Deploy from the Azure CLI

RG="headscale-prod"; LOCATION="eastus"; VM_NAME="headscale-01" IMAGE_URN="sac1647283583153:headscale-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name headscale-vnet --address-prefix 10.100.0.0/16 --subnet-name headscale-subnet --subnet-prefix 10.100.1.0/24 az network nsg create -g "$RG" --name headscale-nsg az network nsg rule create -g "$RG" --nsg-name headscale-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name headscale-nsg --name allow-https --priority 110 \
  --destination-port-ranges 443 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name headscale-nsg --name allow-http --priority 120 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:headscale-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name headscale-vnet --subnet headscale-subnet --nsg headscale-nsg --public-ip-sku Standard
3

Verify the Headscale and nginx services

Connect over SSH, then confirm that both services are active and that Headscale is bound to loopback only.

In the listener output, 127.0.0.1:8080 (Headscale) is loopback only, while nginx listens publicly on :443 and :80:

sudo systemctl status headscale nginx --no-pager | head -14 ss -tln | grep -E ':8080|:443|:80 '
4

Retrieve your per-instance API key

First boot generates the per-instance API key and the control server URL and stores them in a root-only file. Read it over SSH:

sudo cat /root/headscale-credentials.txt

The file contains the control server URL (https://<your public ip>), the per-instance REST API key and quick-start commands for registering a node. Store the API key somewhere safe, because it is unique to this VM.

5

Check the health endpoint and confirm that the API rejects anonymous access

The /health and /healthz endpoints are served without authentication and return HTTP 200, which suits load balancer and Azure health probes.

The REST API requires the per-instance API key. An anonymous request returns HTTP 401, and only a request with the key returns 200.

The self-signed certificate lives at /etc/nginx/tls/headscale.crt, so pass it with --cacert:

CA=/etc/nginx/tls/headscale.crt curl -s -o /dev/null -w 'health: HTTP %{http_code}\n' --cacert $CA https://127.0.0.1/health
KEY=$(sudo grep '^headscale.api_key=' /root/headscale-credentials.txt | cut -d= -f2-)
echo "# Anonymous REST API request (no key) must be rejected:" curl -s -o /dev/null -w 'anonymous /api/v1/apikey -> HTTP %{http_code}\n' --cacert $CA https://127.0.0.1/api/v1/apikey
echo "# Authenticated request with the per instance API key:" curl -s -o /dev/null -w 'authenticated /api/v1/apikey -> HTTP %{http_code}\n' -H "Authorization: Bearer $KEY" --cacert $CA https://127.0.0.1/api/v1/apikey
echo "# List registered nodes over the REST API:" curl -s -H "Authorization: Bearer $KEY" --cacert $CA https://127.0.0.1/api/v1/node

The health endpoint returns HTTP 200. The anonymous API request returns HTTP 401. The authenticated request returns HTTP 200 with the node list as JSON. The control API has no anonymous access.

6

Create a user and a pre-auth key

Machines join the network with a pre-auth key that you generate. No pre-auth key ships in the image, so you create one on demand.

Create a user (a namespace for a set of machines), then generate a reusable pre-auth key for it. The Headscale CLI talks to the running server over a local unix socket, so run these commands directly on the VM:

sudo headscale users create ops
sudo headscale users list
sudo headscale preauthkeys create --user 1 --reusable --expiration 24h

headscale users list shows each user with a numeric id. Pass that id to preauthkeys create.

The command prints a pre-auth key beginning with hskey-auth-. Copy it for the next step. Use --expiration to limit how long the key can be used, and drop --reusable for a single-use key.

7

Join a machine to your network

On any machine running the Tailscale client, point it at your control server with the pre-auth key from Step 6.

The control server uses a self-signed certificate, so first trust that certificate on the client. Alternatively, replace it with your own CA-signed certificate (see Step 9). Replace <public-ip> with your VM public IP or DNS name.

Copy the certificate from the VM and install it into the client trust store:

scp azureuser@<public-ip>:/etc/nginx/tls/headscale.crt headscale.crt
sudo cp headscale.crt /usr/local/share/ca-certificates/headscale.crt
sudo update-ca-certificates

Then bring the client up against your control server:

sudo tailscale up --login-server https://<public-ip> --authkey <your-preauth-key>

Back on the control server, the new machine appears in the node list with an address from the 100.64.0.0/10 range:

sudo headscale nodes list

The screenshot below shows the full flow on the control server. A user and a pre-auth key are created, a Tailscale client registers with that key, and headscale nodes list shows the joined node online with its 100.64 address.

8

How the control server is wired

The Headscale configuration at /etc/headscale/config.yaml binds the control API to loopback only, sets the per-VM server URL, and allocates addresses from the Tailscale ranges.

nginx is the only network-facing surface. It terminates TLS and reverse proxies to Headscale with the directives the control protocol needs.

grep -E '^server_url|^listen_addr|base_domain|v4:|v6:' /etc/headscale/config.yaml
grep -nE 'listen 443 ssl|ssl_certificate|proxy_pass|proxy_buffering|proxy_read_timeout|return 301' /etc/nginx/sites-available/sac-headscale

The output shows Headscale on 127.0.0.1:8080, the server_url set to your VM public URL, the 100.64.0.0/10 and fd7a:115c:a1e0::/48 allocation ranges, and the nginx TLS and reverse proxy directives (listen 443 ssl, ssl_certificate, proxy_pass to 127.0.0.1:8080, proxy_buffering off, proxy_read_timeout 900s).

9

Use your own domain and a trusted certificate

The self-signed certificate is a convenience for getting started. For production, front the control server with your own DNS name and a CA-signed certificate, so clients trust it without any manual step.

  1. Point a DNS record (for example headscale.example.com) at the VM public IP.
  2. Replace /etc/nginx/tls/headscale.crt and /etc/nginx/tls/headscale.key with your CA-signed certificate and key, for example from Let's Encrypt or your own certificate authority.
  3. Update server_url in /etc/headscale/config.yaml to https://headscale.example.com.
  4. Run sudo nginx -t && sudo systemctl reload nginx && sudo systemctl restart headscale.

Clients then join with sudo tailscale up --login-server https://headscale.example.com --authkey <key>, with no certificate trust step.

10

Manage nodes, routes and keys

Headscale gives you full control over the network from the CLI and the REST API:

sudo headscale nodes list
sudo headscale nodes list-routes
sudo headscale apikeys list
sudo headscale preauthkeys list
  • Approve advertised subnet routes with sudo headscale nodes approve-routes.
  • Expire a node with sudo headscale nodes expire.
  • Rotate the API key with sudo headscale apikeys create, followed by sudo headscale apikeys expire.
11

Server components

  • Control server: Headscale (latest stable release, baked reproducibly, see /opt/headscale/VERSION)
  • Control binding: 127.0.0.1:8080 (loopback only, plain HTTP behind the proxy)
  • Front proxy: nginx (TLS on :443, reverse proxy, :80 redirects to HTTPS with a 301)
  • State store: SQLite at /var/lib/headscale/db.sqlite
  • Address ranges: IPv4 100.64.0.0/10, IPv6 fd7a:115c:a1e0::/48
  • Credential: per-instance REST API key, generated on first boot, in /root/headscale-credentials.txt (0600)
  • TLS certificate: self-signed, regenerated per VM on first boot (SAN = public IP and hostname), in /etc/nginx/tls/
  • Operating system: Ubuntu 24.04 LTS (patched at build)
  • License: BSD 3-Clause (Headscale, Copyright (c) 2020 Juan Font)
12

Manage the Headscale service

Use these commands to manage the service:

sudo systemctl status headscale
sudo systemctl restart headscale
sudo journalctl -u headscale -f sudo systemctl reload nginx

Network state lives under /var/lib/headscale and persists across service restarts and VM reboots.

13

Security notes

Restrict the NSG. Allow TCP 443 (and 22 for administration) only from the networks your Tailscale clients connect from. The control server needs to be reachable from the whole internet only if your clients are.

Rotate the API key. Generate a new key with sudo headscale apikeys create and expire the old one with sudo headscale apikeys expire. Update /root/headscale-credentials.txt to match.

Limit your pre-auth keys. Prefer single-use keys and short --expiration windows. Use per-team users to keep node ownership clear.

Use a trusted certificate (Step 9) for production, so clients validate the control server without a manual trust step.

Keep the OS patched. Unattended security upgrades remain enabled on the running VM.

Back up /var/lib/headscale to preserve your network state (users, nodes, keys and routes).

14

Support and licensing

Headscale is distributed under the BSD 3-Clause License (Copyright (c) 2020 Juan Font and contributors). This SecureAnyCloud image bundles the unmodified official release binary.

SecureAnyCloud provides the packaging, hardening, per-instance API key and TLS automation, and 24/7 support with a guaranteed 24-hour response SLA.

Headscale is an independent open-source project and is not affiliated with or endorsed by Tailscale Inc.

Deploy on Azure

Find Headscale on Ubuntu 24.04 on the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.

Deploy on Azure

Launch Headscale on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure