Headscale is an open-source, self-hosted implementation of the Tailscale control server. It provides secure mesh VPN networking for all services in Charon.
Purpose: VPN control server for mesh networking Version: 0.23.0 Port: 8080 (HTTP), 50443 (gRPC) Storage: 1Gi SQLite database Access: External ingress (for enrollment) + VPN mesh
┌─────────────────┐
│ VPN Clients │──┐
└─────────────────┘ │
│
┌─────────────────┐ │ ┌──────────────┐
│ Service Pods │──┼────│ Headscale │
│ (Tailscale │ │ │ Control │
│ sidecars) │ │ │ Server │
└─────────────────┘ │ └──────────────┘
│
┌─────────────────┐ │
│ Your Devices │──┘
└─────────────────┘
# terraform.tfvars
headscale_enabled = true
headscale_version = "0.23.0"
headscale_server_url = "https://vpn.example.com"
headscale_ip_prefix = "100.64.0.0/10"
headscale_magic_dns = true
headscale_storage = "1Gi"See Terraform Variables Reference for full details.
Automatic hostname resolution for VPN-connected devices:
- Pods get hostnames like
grafana.vpn.local - Devices get hostnames like
laptop.vpn.local - No manual DNS configuration needed
Automated service enrollment using pre-auth keys:
- Services automatically generate their own keys
- Keys stored in Kubernetes secrets
- Configurable expiration (default: 90d)
Automatic cleanup of offline nodes:
- Post-deployment script removes stale nodes
- Prevents VPN IP exhaustion
- Cleans up DNS records automatically
kubectl exec -n dev headscale-0 -- headscale nodes listOutput shows:
- Node ID
- Node name
- IP address (100.64.0.X)
- Online status
- Last seen
# Create reusable key with 90-day expiration
kubectl exec -n dev headscale-0 -- headscale preauthkeys create \
--user default --reusable --expiration 90dkubectl exec -n dev headscale-0 -- headscale users create <username>kubectl exec -n dev headscale-0 -- headscale nodes delete --identifier <node-id>kubectl exec -n dev headscale-0 -- headscale routes listDefault: 100.64.0.0/10 (CGNAT range)
- First node: 100.64.0.1
- Subsequent allocations: 100.64.0.2, 100.64.0.3, etc.
- 8080/tcp - HTTP API (internal)
- 50443/tcp - gRPC API (internal)
- 443/tcp - External HTTPS access (via ingress)
Two ingress resources:
- External (
nginx-external) - Public access for enrollment - Internal (
nginx) - VPN-only access for management
kubectl describe pod headscale-0 -n dev
kubectl logs headscale-0 -n devCommon issues:
- PVC not binding (check storage class)
- ConfigMap missing (check Terraform state)
# Check external ingress
kubectl get ingress -n dev headscale-external
# Verify DNS
dig vpn.example.com
# Test endpoint
curl -I https://vpn.example.com/health# Check Tailscale daemon on device
sudo tailscale status
# Check Headscale sees the node
kubectl exec -n dev headscale-0 -- headscale nodes list
# Check firewall allows UDP 41641# Exec into pod
kubectl exec -it headscale-0 -n dev -- sh
# Check database
sqlite3 /var/lib/headscale/db.sqlite "SELECT * FROM nodes;"- API key auto-generated and stored in Kubernetes secret
- External access only for enrollment (web UI)
- All management via VPN or kubectl
- Pre-auth keys have configurable expiration
- TLS certificates via cert-manager
Navigation: Documentation Index | Home