Adds headscale app.

This commit is contained in:
2026-06-19 21:33:05 +00:00
parent 777bab792c
commit 761712ba71
11 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
# Headscale
Headscale is an open-source, self-hosted implementation of the Tailscale control plane. It lets you run your own private WireGuard-based mesh VPN using the Tailscale client.
## Configuration
Key settings in `config.yaml`:
- **domain** - Where the Headscale control plane API is accessible
- **vpnBaseDomain** - Base domain for MagicDNS hostnames assigned to nodes (default: `vpn.{your-cloud-domain}`)
- **storage** - Persistent volume size (default: `2Gi`)
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add headscale
wild app deploy headscale
```
2. Create a user (namespace) in Headscale:
```bash
kubectl exec -n headscale deploy/headscale -- headscale users create <username>
```
3. Generate a pre-auth key for that user:
```bash
kubectl exec -n headscale deploy/headscale -- headscale preauthkeys create --user <user-id> --reusable --expiration 24h
```
Note: `--user` requires the numeric user ID, not the username. Run `headscale users list` to get the ID.
4. On each device you want to join the VPN, install the Tailscale client and connect it to your Headscale instance:
```bash
tailscale up --login-server https://<your-headscale-domain> --authkey <preauthkey>
```
## Registering Nodes via Browser Auth
If a Tailscale client connects without a pre-auth key, it shows an auth URL in its logs. Register that node with:
```bash
kubectl exec -n headscale deploy/headscale -- headscale nodes register --auth-id <hskey-authreq-XXXX> --user <username>
```
Note: `--user` here accepts the username string (not the numeric ID).
## Useful Commands
```bash
# List users
kubectl exec -n headscale deploy/headscale -- headscale users list
# List connected nodes
kubectl exec -n headscale deploy/headscale -- headscale nodes list
# Create a pre-auth key (requires numeric user ID)
kubectl exec -n headscale deploy/headscale -- headscale preauthkeys create --user <id> --reusable
# Delete a node
kubectl exec -n headscale deploy/headscale -- headscale nodes delete --identifier <node-id>
```
## Notes
- Headscale uses SQLite by default — no external database dependency
- Configuration is managed via a ConfigMap (`headscale-config`) — changes require restarting the pod
- In Headscale v0.29+, different commands accept different user identifier types: `preauthkeys` and `users destroy` require the numeric ID, while `nodes register` accepts the username string

View File

@@ -0,0 +1,88 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: headscale-config
namespace: {{ .namespace }}
data:
config.yaml: |
server_url: "https://{{ .domain }}"
listen_addr: "0.0.0.0:8080"
grpc_listen_addr: "0.0.0.0:50443"
grpc_allow_insecure: false
metrics_listen_addr: "127.0.0.1:9090"
# Trust forwarded IPs from Traefik ingress
trusted_proxies:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
log:
level: info
format: text
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
policy:
mode: file
path: /etc/headscale/acls.hujson
tls_letsencrypt_hostname: ""
tls_cert_path: ""
tls_key_path: ""
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v6: fd7a:115c:a1e0::/48
v4: 100.64.0.0/10
allocation: sequential
derp:
server:
enabled: false
region_id: 999
region_code: headscale
region_name: Headscale Embedded DERP
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 24h
dns:
magic_dns: true
base_domain: {{ .vpnBaseDomain }}
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
search_domains: []
node:
ephemeral:
inactivity_timeout: 30m
unix_socket: /var/lib/headscale/headscale.sock
unix_socket_permission: "0770"
acls.hujson: |
{
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
]
}

View File

@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: headscale
namespace: {{ .namespace }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: server
template:
metadata:
labels:
component: server
spec:
securityContext:
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: headscale
image: headscale/headscale:v0.29.1
args: ["serve"]
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: grpc
containerPort: 50443
protocol: TCP
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi
volumeMounts:
- name: headscale-config
mountPath: /etc/headscale
- name: headscale-data
mountPath: /var/lib/headscale
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumes:
- name: headscale-config
configMap:
name: headscale-config
- name: headscale-data
persistentVolumeClaim:
claimName: headscale-data
restartPolicy: Always

View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: headscale
namespace: headscale
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/ttl: "60"
spec:
ingressClassName: traefik
rules:
- host: {{ .domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: headscale
port:
number: 80
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: headscale
labels:
- includeSelectors: true
pairs:
app: headscale
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- configmap.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

View File

@@ -0,0 +1,19 @@
version: 0.29.1-1
scripts:
- name: register-node
description: Register a Tailscale node using the auth ID shown in the Tailscale app.
path: scripts/register-node.sh
params:
- name: AUTH_ID
description: The auth ID shown by the Tailscale app (e.g. hskey-authreq-...)
required: true
- name: USER
description: Headscale username to register the node under
required: true
defaultConfig:
namespace: headscale
domain: headscale.{{ .cloud.domain }}
externalDnsDomain: "{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
storage: 2Gi
vpnBaseDomain: vpn.{{ .cloud.domain }}

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: headscale-data
namespace: headscale
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .storage }}

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
if [ -z "$AUTH_ID" ]; then
echo "Error: AUTH_ID is required"
exit 1
fi
if [ -z "$USER" ]; then
echo "Error: USER is required"
exit 1
fi
NAMESPACE=$(cat manifest.yaml | grep 'namespace:' | head -1 | awk '{print $2}')
echo "Registering node for user '$USER' with auth ID: $AUTH_ID"
kubectl exec -n "$NAMESPACE" deploy/headscale -- \
headscale auth register --auth-id "$AUTH_ID" --user "$USER"

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: headscale
namespace: headscale
spec:
type: ClusterIP
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
- name: grpc
port: 50443
protocol: TCP
targetPort: 50443
selector:
component: server