From 9ec7987763842a5f05cacbe103047098c18d9f4f Mon Sep 17 00:00:00 2001 From: Rich Braun Date: Mon, 3 Aug 2026 14:10:03 -0700 Subject: [PATCH 1/5] SYS-685 headscale chart for VPN --- k8s/helm/headscale/.helmignore | 2 + k8s/helm/headscale/Chart.yaml | 18 +++ .../headscale/subcharts/headplane/Chart.yaml | 15 +++ .../subcharts/headplane/templates/NOTES.txt | 28 +++++ .../subcharts/headplane/templates/app.yaml | 15 +++ .../headplane/templates/configmap.yaml | 19 +++ .../templates/tests/test-connection.yaml | 17 +++ .../headscale/subcharts/headplane/values.yaml | 55 +++++++++ k8s/helm/headscale/templates/NOTES.txt | 28 +++++ k8s/helm/headscale/templates/app.yaml | 15 +++ .../templates/tests/test-connection.yaml | 17 +++ k8s/helm/headscale/values.yaml | 116 ++++++++++++++++++ 12 files changed, 345 insertions(+) create mode 100644 k8s/helm/headscale/.helmignore create mode 100644 k8s/helm/headscale/Chart.yaml create mode 100644 k8s/helm/headscale/subcharts/headplane/Chart.yaml create mode 100644 k8s/helm/headscale/subcharts/headplane/templates/NOTES.txt create mode 100644 k8s/helm/headscale/subcharts/headplane/templates/app.yaml create mode 100644 k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml create mode 100644 k8s/helm/headscale/subcharts/headplane/templates/tests/test-connection.yaml create mode 100644 k8s/helm/headscale/subcharts/headplane/values.yaml create mode 100644 k8s/helm/headscale/templates/NOTES.txt create mode 100644 k8s/helm/headscale/templates/app.yaml create mode 100644 k8s/helm/headscale/templates/tests/test-connection.yaml create mode 100644 k8s/helm/headscale/values.yaml diff --git a/k8s/helm/headscale/.helmignore b/k8s/helm/headscale/.helmignore new file mode 100644 index 00000000..839de881 --- /dev/null +++ b/k8s/helm/headscale/.helmignore @@ -0,0 +1,2 @@ +*~ +.git diff --git a/k8s/helm/headscale/Chart.yaml b/k8s/helm/headscale/Chart.yaml new file mode 100644 index 00000000..2955cbff --- /dev/null +++ b/k8s/helm/headscale/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: headscale +description: Headscale control server for wireshark / tailscale VPN +home: https://github.com/instantlinux/docker-tools +sources: +- https://github.com/instantlinux/docker-tools +- https://github.com/juanfont/headscale +type: application +version: 0.1.0 +appVersion: "0.29.3" +dependencies: +- name: chartlib + version: 0.1.8 + repository: https://instantlinux.github.io/docker-tools +- name: headplane + version: 0.1.0 + repository: file://subcharts/headplane + condition: headplane.enabled diff --git a/k8s/helm/headscale/subcharts/headplane/Chart.yaml b/k8s/helm/headscale/subcharts/headplane/Chart.yaml new file mode 100644 index 00000000..e6e004e5 --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: headplane +description: UI for headscale +home: https://github.com/instantlinux/docker-tools +sources: +- https://github.com/instantlinux/docker-tools +- https://github.com/tale/headplane +type: application +version: 0.1.0 +# specify version tag from ghcr.io in top-level values.yaml +appVersion: "0.0.1" +dependencies: +- name: chartlib + version: 0.1.8 + repository: https://instantlinux.github.io/docker-tools diff --git a/k8s/helm/headscale/subcharts/headplane/templates/NOTES.txt b/k8s/helm/headscale/subcharts/headplane/templates/NOTES.txt new file mode 100644 index 00000000..62ea3f4b --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/templates/NOTES.txt @@ -0,0 +1,28 @@ +{{- if hasKey .Values "service" }} +{{- if or .Values.service.enabled (not (hasKey .Values.service "enabled")) }} +1. Get the application URL by running these commands: +{{- if hasKey .Values "ingress" }} +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "local.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "local.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "local.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "local.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/k8s/helm/headscale/subcharts/headplane/templates/app.yaml b/k8s/helm/headscale/subcharts/headplane/templates/app.yaml new file mode 100644 index 00000000..5a01911b --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/templates/app.yaml @@ -0,0 +1,15 @@ +{{- include "chartlib.configmap" . }} +--- +{{- include "chartlib.deployment" . }} +--- +{{- include "chartlib.hpa" . }} +--- +{{- include "chartlib.ingress" . }} +--- +{{- include "chartlib.ingresstotp" . }} +--- +{{- include "chartlib.service" . }} +--- +{{- include "chartlib.serviceaccount" . }} +--- +{{- include "chartlib.statefulset" . }} diff --git a/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml new file mode 100644 index 00000000..6605336b --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "local.fullname" . }} + labels: + {{- include "local.labels" . | nindent 4 }} +data: + config.yaml: | + server: + host: 0.0.0.0 + port: 3000 + base_url: http://localhost:3000 + cookie_secret_path: /etc/headplane/cookie_secret + cookie_secure: true + headscale: + url: {{ .Values.headscale_url }} + config_path: /etc/headscale/config.yaml + kubernetes: + enabled: true diff --git a/k8s/helm/headscale/subcharts/headplane/templates/tests/test-connection.yaml b/k8s/helm/headscale/subcharts/headplane/templates/tests/test-connection.yaml new file mode 100644 index 00000000..ae159a4f --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +{{- if hasKey .Values "service" }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "local.fullname" . }}-test-connection" + labels: + {{- include "local.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "local.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never +{{- end }} diff --git a/k8s/helm/headscale/subcharts/headplane/values.yaml b/k8s/helm/headscale/subcharts/headplane/values.yaml new file mode 100644 index 00000000..41b985b1 --- /dev/null +++ b/k8s/helm/headscale/subcharts/headplane/values.yaml @@ -0,0 +1,55 @@ +# Default values for headplane +headscale_url: headscale.example.com +deployment: + containerPorts: [ containerPort: 3000 ] + resources: + limits: + memory: 512Mi + requests: + cpu: 300m + memory: 64Mi +volumeMounts: +- mountPath: /etc/headplane/config.yaml + name: config + subPath: config.yaml +- mountPath: /etc/headplane/cookie_secret + name: cookie-secret + readOnly: true + subPath: cookie_secret +- mountPath: /etc/headscale/config.yaml + name: headscale-config + subPath: config.yaml +volumes: +- name: config + configMap: + name: headscale-headplane +- name: headscale-config + configMap: + name: headscale +- name: cookie-secret + secret: + secretName: headscale + items: + - key: cookie_secret + path: cookie_secret + +image: + repository: ghcr.io/tale/headplane + pullPolicy: IfNotPresent + # tag: default + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + enabled: false +service: + clusterIP: None + ports: + - { port: 3000, targetPort: 3000, name: headscale } + type: ClusterIP +autoscaling: + enabled: false + +ingress: + enabled: false diff --git a/k8s/helm/headscale/templates/NOTES.txt b/k8s/helm/headscale/templates/NOTES.txt new file mode 100644 index 00000000..62ea3f4b --- /dev/null +++ b/k8s/helm/headscale/templates/NOTES.txt @@ -0,0 +1,28 @@ +{{- if hasKey .Values "service" }} +{{- if or .Values.service.enabled (not (hasKey .Values.service "enabled")) }} +1. Get the application URL by running these commands: +{{- if hasKey .Values "ingress" }} +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "local.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "local.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "local.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "local.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/k8s/helm/headscale/templates/app.yaml b/k8s/helm/headscale/templates/app.yaml new file mode 100644 index 00000000..5a01911b --- /dev/null +++ b/k8s/helm/headscale/templates/app.yaml @@ -0,0 +1,15 @@ +{{- include "chartlib.configmap" . }} +--- +{{- include "chartlib.deployment" . }} +--- +{{- include "chartlib.hpa" . }} +--- +{{- include "chartlib.ingress" . }} +--- +{{- include "chartlib.ingresstotp" . }} +--- +{{- include "chartlib.service" . }} +--- +{{- include "chartlib.serviceaccount" . }} +--- +{{- include "chartlib.statefulset" . }} diff --git a/k8s/helm/headscale/templates/tests/test-connection.yaml b/k8s/helm/headscale/templates/tests/test-connection.yaml new file mode 100644 index 00000000..ae159a4f --- /dev/null +++ b/k8s/helm/headscale/templates/tests/test-connection.yaml @@ -0,0 +1,17 @@ +{{- if hasKey .Values "service" }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "local.fullname" . }}-test-connection" + labels: + {{- include "local.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "local.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never +{{- end }} diff --git a/k8s/helm/headscale/values.yaml b/k8s/helm/headscale/values.yaml new file mode 100644 index 00000000..48bbb019 --- /dev/null +++ b/k8s/helm/headscale/values.yaml @@ -0,0 +1,116 @@ +# Default values for headscale. + +tlsHostname: headscale.example.com +deployment: + command: [ headscale, serve ] + containerPorts: + - containerPort: 8080 + - containerPort: 9090 +volumeMounts: +- name: config + mountPath: /etc/headscale/config.yaml + subPath: config.yaml +- name: share + mountPath: /var/lib/headscale + subPath: headscale +volumes: +- name: config + configMap: + name: headscale +- name: share + hostPath: { path: /var/lib/docker/k8s-volumes/share } + +image: + repository: headscale/headscale + pullPolicy: IfNotPresent + # tag: default + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: {} +service: + clusterIP: None + ports: + - { port: 8080, targetPort: 8080, name: headscale } + type: ClusterIP + +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/enable-access-log: "false" + rules: + - host: headscale.example.com + http: + paths: + - path: /admin + backend: + service: + name: headscale-headplane + port: + number: 3000 + pathType: Prefix + - path: / + backend: + service: + name: headscale + port: + number: 8080 + pathType: Prefix + +autoscaling: + enabled: false + +configmap: + data: + config.yaml: | + server_url: https://headscale.example.com + listen_addr: 0.0.0.0:8080 + metrics_listen_addr: 0.0.0.0:9090 + noise: + private_key_path: /var/lib/headscale/noise_private.key + prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + 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: 3h + disable_check_updates: false + node: + expire: 0 + ephemeral: + inactivity_timeout: 30m + database: + type: sqlite3 + sqlite: + path: /var/lib/headscale/db.sqlite + write_ahead_log: true + wal_autocheckpoint: 1000 + log: + level: info + format: text + dns: + magic_dns: false + override_local_dns: false + nameservers: + - 1.1.1.1 + - 1.0.0.1 + +headplane: + enabled: true + image: + tag: 0.7.0 From c0bdf26c903162fa4fbe4ae2c308fe26b5ec667e Mon Sep 17 00:00:00 2001 From: Rich Braun Date: Mon, 3 Aug 2026 14:15:39 -0700 Subject: [PATCH 2/5] SYS-685 wip --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e237f2c1..9fab5f85 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ The cluster-deployment tools here include helm charts and ansible playbooks to s | Service | Version | Notes | | --- | --- | --- | +| apache | ** | web server, with webdav example | | authelia | ** | single-signon multi-factor auth | | data-sync | [![](https://img.shields.io/docker/v/instantlinux/data-sync?sort=date)](https://hub.docker.com/r/instantlinux/data-sync "Version badge") | poor-man's SAN for persistent storage | | ddclient | [![](https://img.shields.io/docker/v/instantlinux/ddclient?sort=date)](https://hub.docker.com/r/instantlinux/ddclient "Version badge") | Dynamic DNS client | @@ -68,6 +69,7 @@ The cluster-deployment tools here include helm charts and ansible playbooks to s | haproxy-keepalived | [![](https://img.shields.io/docker/v/instantlinux/haproxy-keepalived?sort=date)](https://hub.docker.com/r/instantlinux/haproxy-keepalived "Version badge") | load balancer | | grafana | ** | monitoring dashboard with prometheus-based alerting | | guacamole | ** | authenticated remote-desktop server | +| headscale | ** | tailscale-compatible VPN control-plane | | mysqldump | [![](https://img.shields.io/docker/v/instantlinux/mysqldump?sort=date)](https://hub.docker.com/r/instantlinux/mysqldump "Version badge") | per-database alternative to xtrabackup | | nagios | [![](https://img.shields.io/docker/v/instantlinux/nagios?sort=date)](https://hub.docker.com/r/instantlinux/nagios "Version badge") | Nagios Core v4 for monitoring | | nagiosql | [![](https://img.shields.io/docker/v/instantlinux/nagiosql?sort=date)](https://hub.docker.com/r/instantlinux/nagiosql "Version badge") | NagiosQL for configuring Nagios Core v4 | @@ -76,6 +78,7 @@ The cluster-deployment tools here include helm charts and ansible playbooks to s | nut-upsd | [![](https://img.shields.io/docker/v/instantlinux/nut-upsd?sort=date)](https://hub.docker.com/r/instantlinux/nut-upsd "Version badge") | Network UPS Tools | | openldap | [![](https://img.shields.io/docker/v/instantlinux/openldap?sort=date)](https://hub.docker.com/r/instantlinux/openldap "Version badge") | OpenLDAP authentication server | | proftpd | [![](https://img.shields.io/docker/v/instantlinux/proftpd?sort=date)](https://hub.docker.com/r/instantlinux/proftpd "Version badge") | FTP server | +| radicale | ** | CalDAV / CardDAV for mobile calendar/contacts sync | | restic | ** | backups | | rsyslogd | [![](https://img.shields.io/docker/v/instantlinux/rsyslogd?sort=date)](https://hub.docker.com/r/instantlinux/rsyslogd "Version badge") | logger in a 13MB image | | samba | [![](https://img.shields.io/docker/v/instantlinux/samba?sort=date)](https://hub.docker.com/r/instantlinux/samba "Version badge") | file server | From 65c86249849871ff0310f73fba3a80fe20d722c2 Mon Sep 17 00:00:00 2001 From: Rich Braun Date: Mon, 3 Aug 2026 14:44:09 -0700 Subject: [PATCH 3/5] SYS-685 wip --- .../headscale/subcharts/headplane/templates/configmap.yaml | 2 +- k8s/helm/headscale/subcharts/headplane/values.yaml | 7 +------ k8s/helm/headscale/values.yaml | 6 ++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml index 6605336b..846a4909 100644 --- a/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml +++ b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml @@ -9,7 +9,7 @@ data: server: host: 0.0.0.0 port: 3000 - base_url: http://localhost:3000 + base_url: {{ .Values.base_url }} cookie_secret_path: /etc/headplane/cookie_secret cookie_secure: true headscale: diff --git a/k8s/helm/headscale/subcharts/headplane/values.yaml b/k8s/helm/headscale/subcharts/headplane/values.yaml index 41b985b1..47bbe127 100644 --- a/k8s/helm/headscale/subcharts/headplane/values.yaml +++ b/k8s/helm/headscale/subcharts/headplane/values.yaml @@ -1,13 +1,8 @@ # Default values for headplane +base_url: http://localhost:3000 headscale_url: headscale.example.com deployment: containerPorts: [ containerPort: 3000 ] - resources: - limits: - memory: 512Mi - requests: - cpu: 300m - memory: 64Mi volumeMounts: - mountPath: /etc/headplane/config.yaml name: config diff --git a/k8s/helm/headscale/values.yaml b/k8s/helm/headscale/values.yaml index 48bbb019..31637782 100644 --- a/k8s/helm/headscale/values.yaml +++ b/k8s/helm/headscale/values.yaml @@ -6,6 +6,12 @@ deployment: containerPorts: - containerPort: 8080 - containerPort: 9090 + resources: + limits: + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi volumeMounts: - name: config mountPath: /etc/headscale/config.yaml From 877eb8a2c6b9f218a99a1a503fbf4c64e8a76f14 Mon Sep 17 00:00:00 2001 From: Rich Braun Date: Tue, 4 Aug 2026 20:39:45 -0700 Subject: [PATCH 4/5] SYS-685 wip --- ansible/roles/network/defaults/main.yml | 1 + ansible/roles/network/tasks/main.yml | 3 +++ ansible/roles/network/tasks/tailscale.yml | 24 +++++++++++++++++++ .../headplane/templates/configmap.yaml | 20 ++++++++-------- k8s/helm/headscale/values.yaml | 12 ++++++++++ .../instances/{restic-minio => restic-remote} | 0 6 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 ansible/roles/network/tasks/tailscale.yml rename k8s/helm/instances/{restic-minio => restic-remote} (100%) diff --git a/ansible/roles/network/defaults/main.yml b/ansible/roles/network/defaults/main.yml index c6e29f0b..4e0bed5d 100644 --- a/ansible/roles/network/defaults/main.yml +++ b/ansible/roles/network/defaults/main.yml @@ -20,6 +20,7 @@ network_defaults: - to: default via: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['gateway'] }}" search: "{{ hostvars[inventory_hostname]['ansible_dns']['search'] }}" + tailscale: false cluster_ip: null vrrp_interface: null diff --git a/ansible/roles/network/tasks/main.yml b/ansible/roles/network/tasks/main.yml index 68bc5162..4045f490 100644 --- a/ansible/roles/network/tasks/main.yml +++ b/ansible/roles/network/tasks/main.yml @@ -9,5 +9,8 @@ - import_tasks: netplan.yml when: ansible_facts['distribution_version'] >= '18.04' +- import_tasks: tailscale.yml + when: network.tailscale + - import_tasks: kernel.yml - import_tasks: sshd.yml diff --git a/ansible/roles/network/tasks/tailscale.yml b/ansible/roles/network/tasks/tailscale.yml new file mode 100644 index 00000000..0b0cc30d --- /dev/null +++ b/ansible/roles/network/tasks/tailscale.yml @@ -0,0 +1,24 @@ +--- +- name: Download Tailscale GPG Key + ansible.builtin.get_url: + dest: /usr/share/keyrings/tailscale-archive-keyring.gpg + url: https://pkgs.tailscale.com/stable/ubuntu/{{ + ansible_facts['distribution_release'] }}.noarmor.gpg + +- name: Add Tailscale repository + ansible.builtin.get_url: + dest: /etc/apt/sources.list.d/tailscale.list + url: https://pkgs.tailscale.com/stable/ubuntu/{{ + ansible_facts['distribution_release'] }}.tailscale-keyring.list + +- name: Install Tailscale + ansible.builtin.apt: + name: tailscale + update_cache: true + state: present + +- name: Start Tailscale + ansible.builtin.systemd: + enabled: yes + name: tailscaled + state: started diff --git a/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml index 846a4909..f2d4c31e 100644 --- a/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml +++ b/k8s/helm/headscale/subcharts/headplane/templates/configmap.yaml @@ -7,13 +7,13 @@ metadata: data: config.yaml: | server: - host: 0.0.0.0 - port: 3000 - base_url: {{ .Values.base_url }} - cookie_secret_path: /etc/headplane/cookie_secret - cookie_secure: true - headscale: - url: {{ .Values.headscale_url }} - config_path: /etc/headscale/config.yaml - kubernetes: - enabled: true + host: 0.0.0.0 + port: 3000 + base_url: {{ .Values.base_url }} + cookie_secret_path: /etc/headplane/cookie_secret + cookie_secure: true + headscale: + url: {{ .Values.headscale_url }} + config_path: /etc/headscale/config.yaml + kubernetes: + enabled: true diff --git a/k8s/helm/headscale/values.yaml b/k8s/helm/headscale/values.yaml index 31637782..f7725b42 100644 --- a/k8s/helm/headscale/values.yaml +++ b/k8s/helm/headscale/values.yaml @@ -48,6 +48,18 @@ ingress: cert-manager.io/cluster-issuer: letsencrypt-prod kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/enable-access-log: "false" + nginx.ingress.kubernetes.io/keepalive-timeout: "300" + nginx.ingress.kubernetes.io/proxy-read-timeout: "86400s" + nginx.ingress.kubernetes.io/proxy-send-timeout: "86400s" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header: Upgrade $http_upgrade; + proxy_set_header: Connection $connection_upgrade; + proxy_set_header: Host $server_name; + 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 $scheme; + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" rules: - host: headscale.example.com http: diff --git a/k8s/helm/instances/restic-minio b/k8s/helm/instances/restic-remote similarity index 100% rename from k8s/helm/instances/restic-minio rename to k8s/helm/instances/restic-remote From 317b757f261fce6e9fe6989aca504692c5e90de0 Mon Sep 17 00:00:00 2001 From: Rich Braun Date: Tue, 4 Aug 2026 20:41:25 -0700 Subject: [PATCH 5/5] SYS-685 wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fab5f85..b38db454 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The cluster-deployment tools here include helm charts and ansible playbooks to s * Calico or flannel networking * ingress-nginx * Local-volume sync -* Minio object storage with prometheus metrics +* Garage object storage * Pod security policies * Automatic certificate issuing/renewal with Letsencrypt * Grafana with prometheus-based alerting