Skip to content
6 changes: 6 additions & 0 deletions helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: headplane
description: "The headplane Helm chart provides an easy way to deploy a Headscale UI with headplane, including an embedded Tailscale relay. This chart simplifies the setup of a private networking solution using Kubernetes."
type: application
version: 0.1.39
appVersion: "0.6.0"
178 changes: 178 additions & 0 deletions helm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# headplane

![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.6.0](https://img.shields.io/badge/AppVersion-0.6.0-informational?style=flat-square)

This helm chart deploys [Headplane](https://github.com/tale/headplane) + [Headscale](https://github.com/juanfont/headscale) and an embedded Tailscale relay in a kubernetes cluster.

## Installation

### Prerequisites
- Kubernetes cluster
- Helm installed

### Install the Chart
```sh
# Install with default values
helm install headplane oci://harbor.lag0.com.br/library/headplane
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise a good overall PR, wondering why we're defaulting to the Harbor hosted instance, is there not a way to just pull it directly from the GitHub repository or publish a chart repository using GitHub pages?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use Harbor just because I like self-hosting stuff lol, no objections for creating a workflow to push this to another chart repository, maybe you can setup a workflow based on this one: https://github.com/antoniolago/headplane-chart/blob/main/.github/workflows/publish-helm-chart.yml (tests are included in this PR already)


# Install with custom values
helm install headplane oci://harbor.lag0.com.br/library/headplane -f values.yaml

```

### Upgrade the Chart
```sh
helm upgrade headplane oci://harbor.lag0.com.br/library/headplane
```

* Some config changes may require manual pod restart to take place

### Uninstall the Chart
```sh
helm uninstall headplane
```

## Values

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| headplane.config.headscale.config_path | string | `"/etc/headscale/config.yaml"` | |
| headplane.config.headscale.config_strict | string | `"true"` | |
| headplane.config.headscale.url | string | `"https://vpn.example.com"` | |
| headplane.config.integration.kubernetes.enabled | bool | `true` | |
| headplane.config.integration.kubernetes.pod_name | string | `"headplane-0"` | |
| headplane.config.integration.kubernetes.validate_manifest | bool | `true` | |
| headplane.config.server.cookie_secure | bool | `true` | |
| headplane.config.server.host | string | `"0.0.0.0"` | |
| headplane.config.server.port | int | `3000` | |
| headplane.envFrom | list | `[]` | |
| headplane.image | string | `"ghcr.io/tale/headplane:0.6.0"` | |
| headplane.oidc.client_id | string | `"REPLACE_IT_WITH_YOUR_OIDC_CLIENT_ID_FOR_HEADPLANE"` | |
| headplane.oidc.disable_api_key_login | bool | `true` | |
| headplane.oidc.enabled | bool | `false` | |
| headplane.oidc.issuer | string | `"https://your-oidc-issuer-url.com"` | |
| headplane.oidc.redirect_uri | string | `"https://your-headplane-admin-domain.com/admin/oidc/callback"` | |
| headplane.oidc.secret_name | string | `"oidc-secrets"` | |
| headplane.oidc.token_endpoint_auth_method | string | `"client_secret_post"` | |
| headscale.acl | string | `""` | |
| headscale.config.database.debug | bool | `false` | |
| headscale.config.database.sqlite.path | string | `"/etc/headscale/db.sqlite"` | |
| headscale.config.database.type | string | `"sqlite"` | |
| headscale.config.derp.paths | list | `[]` | |
| headscale.config.derp.server.automatically_add_embedded_derp_region | bool | `true` | |
| headscale.config.derp.server.enabled | bool | `false` | |
| headscale.config.derp.server.ipv4 | string | `"1.2.3.4"` | |
| headscale.config.derp.server.ipv6 | string | `"2001:db8::1"` | |
| headscale.config.derp.server.private_key_path | string | `"/var/lib/headscale/derp_server_private.key"` | |
| headscale.config.derp.server.region_code | string | `"headscale"` | |
| headscale.config.derp.server.region_id | int | `999` | |
| headscale.config.derp.server.region_name | string | `"Headscale Embedded DERP"` | |
| headscale.config.derp.server.stun_listen_addr | string | `"0.0.0.0:3478"` | |
| headscale.config.derp.urls[0] | string | `"https://controlplane.tailscale.com/derpmap/default"` | |
| headscale.config.dns.base_domain | string | `"headscale.vpn"` | |
| headscale.config.dns.magic_dns | bool | `true` | |
| headscale.config.dns.nameservers.global[0] | string | `"1.1.1.1"` | |
| headscale.config.dns.nameservers.global[1] | string | `"8.8.8.8"` | |
| headscale.config.grpc_allow_insecure | bool | `false` | |
| headscale.config.grpc_listen_addr | string | `"0.0.0.0:50443"` | |
| headscale.config.listen_addr | string | `"0.0.0.0:8080"` | |
| headscale.config.metrics_listen_addr | string | `"0.0.0.0:9090"` | |
| headscale.config.noise.private_key_path | string | `"/etc/headscale/noise_private.key"` | |
| headscale.config.policy.mode | string | `"database"` | |
| headscale.config.policy.path | string | `"/etc/headscale/acl.hujson"` | |
| headscale.config.prefixes.allocation | string | `"sequential"` | |
| headscale.config.prefixes.v4 | string | `"100.64.0.0/10"` | |
| headscale.config.prefixes.v6 | string | `"fd7a:115c:a1e0::/48"` | |
| headscale.config.server_url | string | `"https://vpn.example.com"` | |
| headscale.envFrom | list | `[]` | |
| headscale.image | string | `"headscale/headscale:0.25.1"` | |
| headscale.oidc.client_id | string | `"YOUR_OIDC_CLIENT_ID_FOR_HEADSCALE"` | |
| headscale.oidc.enabled | bool | `false` | |
| headscale.oidc.issuer | string | `"https://your-oidc-issuer.com"` | |
| headscale.oidc.secret_name | string | `"oidc-secrets"` | |
| ingress.annotations | list | `[]` | |
| ingress.className | string | `"nginx"` | |
| ingress.enabled | bool | `false` | |
| ingress.headplaneDomain | string | `"headplane.example.com"` | |
| ingress.headscaleDomain | string | `"vpn.example.com"` | |
| ingress.labels | list | `[]` | |
| ingress.tlsSecretName | string | `"headplane-tls"` | |
| pvc.accessModes[0] | string | `"ReadWriteOnce"` | |
| pvc.annotations | object | `{}` | |
| pvc.enabled | bool | `true` | |
| pvc.labels | list | `[]` | |
| pvc.name | string | `"headscale-config"` | |
| pvc.storage | string | `"1Gi"` | |
| relay.config.advertise_exit_node | string | `"true"` | |
| relay.config.authKey | string | `""` | |
| relay.config.exit_node | string | `"example.com"` | |
| relay.config.firewall_debug | string | `"false"` | |
| relay.config.hostname | string | `"example.com"` | |
| relay.config.login_server | string | `"https://vpn.example.com"` | |
| relay.config.routes | string | `"10.0.0.0/8"` | |
| relay.enabled | bool | `false` | |
| relay.image | string | `"ghcr.io/tailscale/tailscale:v1.80.3"` | |
| relay.pvc.accessModes[0] | string | `"ReadWriteOnce"` | |
| relay.pvc.enabled | bool | `false` | |
| relay.pvc.name | string | `"tailscale-relay-data"` | |
| relay.pvc.storage | string | `"1Gi"` | |

### OIDC Configuration

To use OIDC, you must provide the OIDC client secrets via Kubernetes secret:

```sh
kubectl create secret generic oidc-secrets \
--from-literal=HEADPLANE_OIDC__CLIENT_SECRET=your-headplane-oidc-client-secret \
--from-literal=HEADPLANE_OIDC__CLIENT_ID=your-headplane-oidc-client-id \
--from-literal=HEADSCALE_OIDC__CLIENT_SECRET=your-headscale-oidc-client-secret \
--from-literal=HEADSCALE_OIDC__CLIENT_ID=your-headscale-oidc-client-id \
-n <namespace>
```

Then enable OIDC in your `values.yaml`:
```yaml
headplane:
oidc:
enabled: true
issuer: "https://your-oidc-issuer-url.com"
redirect_uri: "https://your-headplane-admin-domain.com/admin/oidc/callback"
secret_name: "oidc-secrets" # Name of your OIDC secret

headscale:
oidc:
enabled: true
issuer: "https://your-oidc-issuer.com"
secret_name: "oidc-secrets" # Same secret as Headplane
```

You can add any additional environment variables by creating more secrets or config-maps and adding them to the `envFrom` section. For example, to add custom configuration:

```sh
kubectl create secret generic headplane-custom-config \
--from-literal=CUSTOM_VAR=value \
--from-literal=ANOTHER_VAR=another-value \
-n <namespace>
```

Then add it to your values:
```yaml
headplane:
envFrom:
- secretRef:
name: headplane-custom-config
```

Note: Make sure to keep your secrets secure and never commit them to version control.
Consider using a secrets management solution in production like external-secrets.

## License
Copyright © 2025 antoniolago
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if this isn't licensed, especially as Apache because its language doesn't necessarily fit well with MIT the best.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we can remove Apache license since the code is forked from https://github.com/nbcloudio/headplane-chart unless all contributors agrees, or am I wrong?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then in that case, we do need their permission yes, and I can't technically merge this in-tree until it's been addressed.


Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:

```
http://www.apache.org/licenses/LICENSE-2.0
```

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
101 changes: 101 additions & 0 deletions helm/README.md.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# headplane

![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.6.0](https://img.shields.io/badge/AppVersion-0.6.0-informational?style=flat-square)

This helm chart deploys [Headplane](https://github.com/tale/headplane) + [Headscale](https://github.com/juanfont/headscale) and an embedded Tailscale relay in a kubernetes cluster.

## Installation

### Prerequisites
- Kubernetes cluster
- Helm installed

### Install the Chart
```sh
# Install with default values
helm install headplane oci://harbor.lag0.com.br/library/headplane

# Install with custom values
helm install headplane oci://harbor.lag0.com.br/library/headplane -f values.yaml

```

### Upgrade the Chart
```sh
helm upgrade headplane oci://harbor.lag0.com.br/library/headplane
```

* Some config changes may require manual pod restart to take place

### Uninstall the Chart
```sh
helm uninstall headplane
```

## Values

| Key | Type | Default | Description |
|-----|------|---------|-------------|
{{- range .Values }}
| {{ .Key }} | {{ .Type }} | {{ .Default }} | {{ .Description }} |
{{- end }}

### OIDC Configuration

To use OIDC, you must provide the OIDC client secrets via Kubernetes secret:

```sh
kubectl create secret generic oidc-secrets \
--from-literal=HEADPLANE_OIDC__CLIENT_SECRET=your-headplane-oidc-client-secret \
--from-literal=HEADPLANE_OIDC__CLIENT_ID=your-headplane-oidc-client-id \
--from-literal=HEADSCALE_OIDC__CLIENT_SECRET=your-headscale-oidc-client-secret \
--from-literal=HEADSCALE_OIDC__CLIENT_ID=your-headscale-oidc-client-id \
-n <namespace>
```

Then enable OIDC in your `values.yaml`:
```yaml
headplane:
oidc:
enabled: true
issuer: "https://your-oidc-issuer-url.com"
redirect_uri: "https://your-headplane-admin-domain.com/admin/oidc/callback"
secret_name: "oidc-secrets" # Name of your OIDC secret

headscale:
oidc:
enabled: true
issuer: "https://your-oidc-issuer.com"
secret_name: "oidc-secrets" # Same secret as Headplane
```

You can add any additional environment variables by creating more secrets or config-maps and adding them to the `envFrom` section. For example, to add custom configuration:

```sh
kubectl create secret generic headplane-custom-config \
--from-literal=CUSTOM_VAR=value \
--from-literal=ANOTHER_VAR=another-value \
-n <namespace>
```

Then add it to your values:
```yaml
headplane:
envFrom:
- secretRef:
name: headplane-custom-config
```

Note: Make sure to keep your secrets secure and never commit them to version control.
Consider using a secrets management solution in production like external-secrets.

## License
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concerns as earlier

Copyright © 2025 antoniolago

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:

```
http://www.apache.org/licenses/LICENSE-2.0
```

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10 changes: 10 additions & 0 deletions helm/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{/*
Generate a random cookie secret if none is provided
*/}}
{{- define "headplane.cookieSecret" -}}
{{- if and .Values.headplane.secret .Values.headplane.secret.server (hasKey .Values.headplane.secret.server "cookie_secret") .Values.headplane.secret.server.cookie_secret -}}
{{- .Values.headplane.secret.server.cookie_secret -}}
{{- else -}}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- end -}}
23 changes: 23 additions & 0 deletions helm/templates/configmap-headplane.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: headplane-config
data:
config.yaml: |
server:
{{- toYaml .Values.headplane.config.server | nindent 6 }}
cookie_secret: {{ include "headplane.cookieSecret" . | quote }}
headscale:
{{- toYaml .Values.headplane.config.headscale | nindent 6 }}
integration:
{{- toYaml .Values.headplane.config.integration | nindent 6 }}
# only add oidc if .Values.headplane.config.oidc is set
{{- if .Values.headplane.config.oidc.enabled }}
oidc:
issuer: {{ .Values.headplane.config.oidc.issuer | quote }}
disable_api_key_login: {{ .Values.headplane.config.oidc.disable_api_key_login | quote }}
token_endpoint_auth_method: {{ .Values.headplane.config.oidc.token_endpoint_auth_method | quote }}
redirect_uri: {{ .Values.headplane.config.oidc.redirect_uri | quote }}
client_id: {{ .Values.headplane.config.oidc.client_id | quote }}
{{- end }}
11 changes: 11 additions & 0 deletions helm/templates/configmap-headscale-acl.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if .Values.headscale.acl }}
apiVersion: v1
kind: ConfigMap
metadata:
name: headscale-acl
labels:
app: headplane
data:
acl.hujson: |-
{{- .Values.headscale.acl | nindent 4 }}
{{- end }}
25 changes: 25 additions & 0 deletions helm/templates/configmap-headscale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: headscale-default-config
data:
config.yaml: |
{{- toYaml .Values.headscale.config | nindent 4 }}
{{- if .Values.headscale.config.oidc.enabled }}
oidc:
issuer: {{ .Values.headscale.config.oidc.issuer | quote }}
client_id: {{ .Values.headscale.config.oidc.client_id | quote }}
{{- if .Values.headscale.config.oidc.allowed_groups }}
allowed_groups:
{{- toYaml .Values.headscale.config.oidc.allowed_groups | nindent 8 }}
{{- end }}
{{- if .Values.headscale.config.oidc.allowed_domains }}
allowed_domains:
{{- toYaml .Values.headscale.config.oidc.allowed_domains | nindent 8 }}
{{- end }}
{{- if .Values.headscale.config.oidc.allowed_users }}
allowed_users:
{{- toYaml .Values.headscale.config.oidc.allowed_users | nindent 8 }}
{{- end }}
{{- end }}
Loading