Headscale deployment¶
Runbook for deploying Headscale as the self-hosted control plane for the Golem Trust zero-trust network. Headscale replaces Tailscale’s cloud coordination service; every node runs the standard Tailscale client but registers with the Golem Trust Headscale instance instead of with Tailscale’s servers. This satisfies Mr. Bent’s requirement that all coordination traffic remain within Golem Trust’s infrastructure and jurisdiction.
Architecture¶
Headscale runs on a dedicated Hetzner CX21 instance in Helsinki at headscale.golemtrust.am (10.0.5.1). It is not itself on the Tailscale network it manages; it sits outside it and coordinates it. All production nodes register with Headscale at https://headscale.golemtrust.am:443 and receive WireGuard configuration that enables direct peer-to-peer connections.
When direct connections are not possible (NAT, firewall traversal, or nodes in different datacentres that have not yet established a direct path), traffic is relayed through the DERP servers (see the DERP server setup runbook). DERP traffic is encrypted end-to-end with WireGuard; the DERP server cannot read it.
Prerequisites¶
A Hetzner CX21 instance running Debian 12 at
headscale.golemtrust.amDNS A record for
headscale.golemtrust.ampointing to the instance’s public IPTLS certificate for
headscale.golemtrust.am(Certbot with Cloudflare DNS)PostgreSQL database
headscaleand userheadscaleondb.golemtrust.am
Installation¶
HEADSCALE_VERSION="0.23.0"
wget "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb"
dpkg -i "headscale_${HEADSCALE_VERSION}_linux_amd64.deb"
Headscale installs as a systemd service. It does not start automatically; configure it first.
Configuration¶
Create the configuration directory and copy the default configuration:
mkdir -p /etc/headscale
cp /usr/share/doc/headscale/config-example.yaml /etc/headscale/config.yaml
Edit /etc/headscale/config.yaml. Key settings:
server_url: https://headscale.golemtrust.am
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
private_key_path: /var/lib/headscale/private.key
noise:
private_key_path: /var/lib/headscale/noise_private.key
ip_prefixes:
- 100.64.0.0/10
db_type: postgres
db_host: db.golemtrust.am
db_port: 5432
db_name: headscale
db_user: headscale
db_pass: "<retrieve from Vaultwarden, collection: Infrastructure>"
tls_cert_path: /etc/letsencrypt/live/headscale.golemtrust.am/fullchain.pem
tls_key_path: /etc/letsencrypt/live/headscale.golemtrust.am/privkey.pem
dns_config:
magic_dns: true
base_domain: ts.golemtrust.am
nameservers:
- 1.1.1.1
derp:
server:
enabled: false
urls:
- https://headscale.golemtrust.am/derpmap.json
auto_update_enabled: false
update_frequency: 24h
acls_policy_path: /etc/headscale/acls.hujson
log:
level: warn
The ip_prefixes range 100.64.0.0/10 is the Carrier-Grade NAT range used by Tailscale. All nodes on the Tailscale network receive addresses in this range. The range does not overlap with the Hetzner private network (10.0.0.0/8), which remains for direct server-to-server communication where Headscale is not involved.
The DERP map is served by Headscale itself. The derpmap.json configuration is documented in the DERP server setup runbook.
Starting Headscale¶
systemctl enable headscale
systemctl start headscale
headscale version
Creating namespaces¶
Headscale organises nodes into namespaces (called “users” in newer versions). Create namespaces for each logical group:
headscale users create infrastructure
headscale users create banking-ops
headscale users create soc-team
headscale users create vendor-access
Nodes are registered into a specific namespace. ACL rules reference namespace names to control which namespaces can reach which services.
Registering nodes¶
On each server that should join the zero-trust network, install the Tailscale client:
curl -fsSL https://tailscale.com/install.sh | sh
Register the node with Headscale rather than Tailscale’s cloud. Generate a pre-authentication key for the relevant namespace:
headscale preauthkeys create --user infrastructure --expiration 1h --reusable
On the server being registered:
tailscale up --login-server https://headscale.golemtrust.am \
--authkey <preauthkey> \
--hostname auth-server \
--accept-routes
Confirm the node appears in Headscale:
headscale nodes list
Repeat for every production server. Assign each server to the infrastructure namespace. Banking operations workstations go in banking-ops. SOC team workstations go in soc-team.
Nginx reverse proxy¶
Headscale’s gRPC and HTTP interfaces need to be reachable on standard HTTPS. Create /etc/nginx/sites-available/headscale:
server {
listen 80;
server_name headscale.golemtrust.am;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name headscale.golemtrust.am;
ssl_certificate /etc/letsencrypt/live/headscale.golemtrust.am/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/headscale.golemtrust.am/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Verification¶
List all registered nodes and confirm they show as online:
headscale nodes list
From any registered node, confirm connectivity to another:
tailscale ping auth-server
tailscale status
The tailscale ping result should show a direct connection or a DERP relay path. Once the DERP servers are operational (see the DERP server setup runbook), all nodes should be able to reach each other regardless of their network position.