A Kubernetes operator for managing Headscale - an open source, self-hosted implementation of the Tailscale control server.
The Headscale Operator simplifies the deployment and management of Headscale instances on Kubernetes. It provides a declarative, GitOps-friendly way to configure and deploy Headscale with all its configuration options through Kubernetes Custom Resources.
- Declarative Configuration: Define your entire Headscale setup as a Kubernetes Custom Resource
- Automatic Deployment: Manages StatefulSets, Services, ConfigMaps, and PersistentVolumes
- API Key Management: Automatic API key creation and rotation with configurable expiration
- Full Config Support: Supports all Headscale configuration options including:
- Database configuration (SQLite/PostgreSQL)
- DERP server configuration
- DNS and MagicDNS settings
- OIDC authentication
- TLS/Let's Encrypt integration
- Policy configuration
- Observability: Built-in metrics endpoint for monitoring
- Production Ready: Supports high availability with persistent storage
- Headscale Operator
- Go 1.25.0+
- Docker 17.03+
- kubectl 1.11.3+
- Access to a Kubernetes 1.11.3+ cluster
helm install headscale-operator oci://ghcr.io/infradohq/headscale-operator/charts/headscale-operator:$LATEST_VERSION- Create a namespace for Headscale:
kubectl create namespace headscale- Create a Headscale instance:
apiVersion: headscale.infrado.cloud/v1beta1
kind: Headscale
metadata:
name: headscale-sample
namespace: headscale
spec:
version: "v0.27.0"
replicas: 1
config:
server_url: http://vpn.headscale.local
grpc_allow_insecure: true
derp:
server:
enabled: false
disable_check_updates: false
database:
type: sqlite
dns:
magic_dns: false
# Automatic API key management (optional)
apiKey:
autoManage: true # Automatically create and rotate API keys
secretName: headscale-api-key
expiration: "2160h" # API key expires in 90 days (2160 hours)
rotationBuffer: "240h" # Rotate 10 days (240 hours) before expirationApply the configuration:
kubectl apply -f config/samples/headscale_v1beta1_headscale.yamlOr use kustomize:
kubectl apply -k config/samples/The operator will automatically create and manage the following resources:
- A StatefulSet running Headscale
- A ConfigMap with the Headscale configuration
- Services for HTTP, gRPC, and metrics endpoints
- PersistentVolumeClaims for data storage
- API key management sidecar (if enabled)
- Kubernetes Secret with the API key (if auto-managed)
The operator provides the HeadscaleUser custom resource to manage users in your Headscale instance.
apiVersion: headscale.infrado.cloud/v1beta1
kind: HeadscaleUser
metadata:
name: alice
namespace: headscale
spec:
# Reference to the Headscale instance
headscaleRef: headscale-sample
# Username (immutable after creation)
username: alice
# Optional: Display name for the user
displayName: Alice Smith
# Optional: Email address
email: alice@example.com
# Optional: Profile picture URL
pictureURL: https://example.com/alice.jpgApply the user:
kubectl apply -f headscaleuser.yaml- username: Must be unique and follow DNS label rules (lowercase alphanumeric with hyphens). This field is immutable after creation.
- displayName: Human-readable name (max 255 characters). Immutable after creation.
- email: Valid email address (max 320 characters). Immutable after creation.
- pictureURL: HTTP(S) URL to profile picture (max 2048 characters). Immutable after creation.
# List all users
kubectl get headscaleuser -n headscale
# Get user details
kubectl get headscaleuser alice -n headscale -o yaml
# View user status including Headscale UserID
kubectl get headscaleuser alice -n headscale -o jsonpath='{.status.userId}'The operator provides the HeadscalePreAuthKey custom resource to manage pre-authentication keys for registering nodes.
apiVersion: headscale.infrado.cloud/v1beta1
kind: HeadscalePreAuthKey
metadata:
name: dev-key
namespace: headscale
spec:
# Reference to the Headscale instance
headscaleRef: headscale-sample
# Reference to a HeadscaleUser resource
headscaleUserRef: alice
# Alternatively, specify user ID directly:
# userId: 1
# Key expires after 24 hours
expiration: "24h"
# Can be used only once (set to true for multiple uses)
reusable: false
# Creates ephemeral nodes (automatically removed when disconnected)
ephemeral: false
# Automatically assign tags to nodes using this key
tags:
- "tag:dev"
- "tag:laptop"
# Optional: Secret name (defaults to resource name)
secretName: alice-dev-keyApply the preauth key:
kubectl apply -f headscalepreauthkey.yaml- headscaleRef: Name of the Headscale instance (required)
- headscaleUserRef: Name of the HeadscaleUser resource (use this OR userId)
- userId: Numeric user ID from Headscale (use this OR headscaleUserRef)
- expiration: Duration string (e.g., "30m", "24h", "1h30m"). Default: "1h"
- reusable: Whether key can be used multiple times. Default: false
- ephemeral: Whether nodes should be ephemeral. Default: false
- tags: List of tags to assign (format: "tag:name")
- secretName: Name of the Kubernetes secret to store the key. Defaults to the resource name.
The generated preauth key is stored in a Kubernetes Secret:
# Get the preauth key
kubectl get secret alice-dev-key -n headscale -o jsonpath='{.data.key}' | base64 -d
# View all preauth keys
kubectl get headscalepreauthkey -n headscale
# View details
kubectl get headscalepreauthkey dev-key -n headscale -o yamlUse the retrieved key to register a new node to your Headscale network:
# Get the key
KEY=$(kubectl get secret alice-dev-key -n headscale -o jsonpath='{.data.key}' | base64 -d)
# Register a node using Tailscale client
tailscale up --login-server=https://headscale.example.com --authkey=$KEYOne-time use key for a single device:
apiVersion: headscale.infrado.cloud/v1beta1
kind: HeadscalePreAuthKey
metadata:
name: laptop-key
spec:
headscaleRef: headscale-sample
headscaleUserRef: alice
expiration: "1h"
reusable: falseReusable key for multiple CI/CD runners:
apiVersion: headscale.infrado.cloud/v1beta1
kind: HeadscalePreAuthKey
metadata:
name: ci-runner-key
spec:
headscaleRef: headscale-sample
headscaleUserRef: ci-user
expiration: "720h" # 30 days
reusable: true
ephemeral: true # Auto-cleanup disconnected runners
tags:
- "tag:ci"
- "tag:ephemeral"Key for temporary test environments:
apiVersion: headscale.infrado.cloud/v1beta1
kind: HeadscalePreAuthKey
metadata:
name: test-env-key
spec:
headscaleRef: headscale-sample
userId: 5
expiration: "2h"
ephemeral: true
tags:
- "tag:test"When API key auto-management is enabled, the sidecar creates a Kubernetes Secret containing the API key:
# Get the API key
kubectl get secret headscale-api-key -n headscale -o jsonpath='{.data.api-key}' | base64 -d
# View full secret details
kubectl get secret headscale-api-key -n headscale -o yamlThe secret contains:
api-key: The actual API key for authenticating with Headscaleexpiration: When the API key will expire (RFC3339 format)created-at: When the API key was created (RFC3339 format)
For more details on API key management, see cmd/apikey-manager/README.md.
Delete the Headscale instance:
kubectl delete -k config/samples/To completely remove the operator:
helm uninstall headscale-operator# Build the operator binary
make build
# Run tests
make test
# Build Docker image
make docker-build IMG=<registry>/headscale-operator:tag# Install CRDs
make install
# Run the operator locally (outside the cluster)
make run
# In another terminal, create a sample Headscale instance
kubectl apply -f config/samples/headscale_v1beta1_headscale.yaml# Build and push image
make docker-build docker-push IMG=<registry>/headscale-operator:tag
# Deploy to cluster
make deploy IMG=<registry>/headscale-operator:tag# Run unit tests
make test
# Run end-to-end tests
make test-e2eWe welcome contributions! Here's how you can help:
- Fork the repository and create your branch from
main - Make your changes and add tests if applicable
- Ensure tests pass by running
make test - Format your code with
make fmtandmake vet - Commit your changes using conventional commits
- Open a pull request with a clear description of your changes
- Headscale - The awesome project this operator manages
- Kubebuilder - The framework used to build this operator
- All our contributors