From 761712ba71a8eff4382a9dcc3d9735e8445b99f1 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Fri, 19 Jun 2026 21:33:05 +0000 Subject: [PATCH] Adds headscale app. --- headscale/app.yaml | 5 ++ headscale/versions/1/README.md | 65 ++++++++++++++ headscale/versions/1/configmap.yaml | 88 +++++++++++++++++++ headscale/versions/1/deployment.yaml | 73 +++++++++++++++ headscale/versions/1/ingress.yaml | 26 ++++++ headscale/versions/1/kustomization.yaml | 16 ++++ headscale/versions/1/manifest.yaml | 19 ++++ headscale/versions/1/namespace.yaml | 4 + headscale/versions/1/pvc.yaml | 11 +++ headscale/versions/1/scripts/register-node.sh | 18 ++++ headscale/versions/1/service.yaml | 18 ++++ 11 files changed, 343 insertions(+) create mode 100644 headscale/app.yaml create mode 100644 headscale/versions/1/README.md create mode 100644 headscale/versions/1/configmap.yaml create mode 100644 headscale/versions/1/deployment.yaml create mode 100644 headscale/versions/1/ingress.yaml create mode 100644 headscale/versions/1/kustomization.yaml create mode 100644 headscale/versions/1/manifest.yaml create mode 100644 headscale/versions/1/namespace.yaml create mode 100644 headscale/versions/1/pvc.yaml create mode 100755 headscale/versions/1/scripts/register-node.sh create mode 100644 headscale/versions/1/service.yaml diff --git a/headscale/app.yaml b/headscale/app.yaml new file mode 100644 index 0000000..8109f81 --- /dev/null +++ b/headscale/app.yaml @@ -0,0 +1,5 @@ +name: headscale +is: headscale +description: Headscale is a self-hosted, open-source implementation of the Tailscale control server, enabling you to manage your own private WireGuard-based mesh VPN network. +icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/headscale.svg +latest: "1" diff --git a/headscale/versions/1/README.md b/headscale/versions/1/README.md new file mode 100644 index 0000000..abf5567 --- /dev/null +++ b/headscale/versions/1/README.md @@ -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 + ``` + +3. Generate a pre-auth key for that user: + ```bash + kubectl exec -n headscale deploy/headscale -- headscale preauthkeys create --user --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:// --authkey + ``` + +## 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 --user +``` +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 --reusable + +# Delete a node +kubectl exec -n headscale deploy/headscale -- headscale nodes delete --identifier +``` + +## 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 diff --git a/headscale/versions/1/configmap.yaml b/headscale/versions/1/configmap.yaml new file mode 100644 index 0000000..1cbd4c5 --- /dev/null +++ b/headscale/versions/1/configmap.yaml @@ -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": ["*:*"] + } + ] + } diff --git a/headscale/versions/1/deployment.yaml b/headscale/versions/1/deployment.yaml new file mode 100644 index 0000000..0f41a1d --- /dev/null +++ b/headscale/versions/1/deployment.yaml @@ -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 diff --git a/headscale/versions/1/ingress.yaml b/headscale/versions/1/ingress.yaml new file mode 100644 index 0000000..01b928e --- /dev/null +++ b/headscale/versions/1/ingress.yaml @@ -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 }} diff --git a/headscale/versions/1/kustomization.yaml b/headscale/versions/1/kustomization.yaml new file mode 100644 index 0000000..8a9dc41 --- /dev/null +++ b/headscale/versions/1/kustomization.yaml @@ -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 diff --git a/headscale/versions/1/manifest.yaml b/headscale/versions/1/manifest.yaml new file mode 100644 index 0000000..50f9f45 --- /dev/null +++ b/headscale/versions/1/manifest.yaml @@ -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 }} diff --git a/headscale/versions/1/namespace.yaml b/headscale/versions/1/namespace.yaml new file mode 100644 index 0000000..b7816b8 --- /dev/null +++ b/headscale/versions/1/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: "{{ .namespace }}" diff --git a/headscale/versions/1/pvc.yaml b/headscale/versions/1/pvc.yaml new file mode 100644 index 0000000..c10dc0c --- /dev/null +++ b/headscale/versions/1/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: headscale-data + namespace: headscale +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .storage }} diff --git a/headscale/versions/1/scripts/register-node.sh b/headscale/versions/1/scripts/register-node.sh new file mode 100755 index 0000000..e842f5e --- /dev/null +++ b/headscale/versions/1/scripts/register-node.sh @@ -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" diff --git a/headscale/versions/1/service.yaml b/headscale/versions/1/service.yaml new file mode 100644 index 0000000..bb005c2 --- /dev/null +++ b/headscale/versions/1/service.yaml @@ -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