This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-helm-chart.git
The following commit(s) were added to refs/heads/master by this push:
new 02b02e5 Add support for generating JWT secrets during install (#672)
02b02e5 is described below
commit 02b02e573d5cddf473b5790ac36c332f3e94e3e5
Author: Shaun Becker <[email protected]>
AuthorDate: Tue Apr 21 09:53:40 2026 -0400
Add support for generating JWT secrets during install (#672)
---
.ci/clusters/values-jwt-generated.yaml | 46 +++++
.ci/helm.sh | 11 +-
.github/workflows/pulsar-helm-chart-ci.yaml | 6 +
charts/pulsar/templates/jwt-secret-init.yaml | 253 +++++++++++++++++++++++++++
charts/pulsar/values.yaml | 28 +++
5 files changed, 341 insertions(+), 3 deletions(-)
diff --git a/.ci/clusters/values-jwt-generated.yaml
b/.ci/clusters/values-jwt-generated.yaml
new file mode 100644
index 0000000..9d29a10
--- /dev/null
+++ b/.ci/clusters/values-jwt-generated.yaml
@@ -0,0 +1,46 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+
+auth:
+ authentication:
+ enabled: true
+ jwt:
+ # Enable JWT authentication
+ enabled: true
+ # If the token is generated by a secret key, set the usingSecretKey as
true.
+ # If the token is generated by a private key, set the usingSecretKey as
false.
+ usingSecretKey: false
+ generateSecrets:
+ enabled: true
+ authorization:
+ enabled: true
+ superUsers:
+ # broker to broker communication
+ broker: "broker-admin"
+ # proxy to broker communication
+ proxy: "proxy-admin"
+ # pulsar-admin client to broker/proxy communication
+ client: "admin"
+ # pulsar-manager to broker communication
+ manager: "manager-admin"
+
+components:
+ pulsar_manager: true
+
diff --git a/.ci/helm.sh b/.ci/helm.sh
index 8e5be40..574e602 100755
--- a/.ci/helm.sh
+++ b/.ci/helm.sh
@@ -22,6 +22,7 @@ PULSAR_HOME="$(cd "${BINDIR}/.." && pwd)"
CHARTS_HOME=${PULSAR_HOME}
PULSAR_CHART_LOCAL=${CHARTS_HOME}/charts/pulsar
PULSAR_CHART_VERSION=${PULSAR_CHART_VERSION:-"local"}
+SKIP_PREPARE_RELEASE=${SKIP_PREPARE_RELEASE:-0}
OUTPUT_BIN=${CHARTS_HOME}/output/bin
# shellcheck disable=SC2034
KIND_BIN=$OUTPUT_BIN/kind
@@ -136,9 +137,13 @@ function ci::install_pulsar_chart() {
echo "Installing the pulsar chart"
${KUBECTL} create namespace "${NAMESPACE}"
ci::install_cert_manager
- echo "${CHARTS_HOME}"/scripts/pulsar/prepare_helm_release.sh -k
"${CLUSTER}" -n "${NAMESPACE}" "${extra_opts[@]}"
- "${CHARTS_HOME}"/scripts/pulsar/prepare_helm_release.sh -k "${CLUSTER}"
-n "${NAMESPACE}" "${extra_opts[@]}"
- sleep 10
+ if [[ "${SKIP_PREPARE_RELEASE}" == "1" ]]; then
+ echo "Skipping ${CHARTS_HOME}/scripts/pulsar/prepare_helm_release.sh"
+ else
+ echo "${CHARTS_HOME}"/scripts/pulsar/prepare_helm_release.sh -k
"${CLUSTER}" -n "${NAMESPACE}" "${extra_opts[@]}"
+ "${CHARTS_HOME}"/scripts/pulsar/prepare_helm_release.sh -k
"${CLUSTER}" -n "${NAMESPACE}" "${extra_opts[@]}"
+ sleep 10
+ fi
# install metallb for loadbalancer support
# following instructions from
https://kind.sigs.k8s.io/docs/user/loadbalancer/
diff --git a/.github/workflows/pulsar-helm-chart-ci.yaml
b/.github/workflows/pulsar-helm-chart-ci.yaml
index 8cd9489..94d1d53 100644
--- a/.github/workflows/pulsar-helm-chart-ci.yaml
+++ b/.github/workflows/pulsar-helm-chart-ci.yaml
@@ -209,6 +209,9 @@ jobs:
- name: JWT Symmetric Key
values_file: .ci/clusters/values-jwt-symmetric.yaml
shortname: jwt-symmetric
+ - name: JWT Generated Keys
+ values_file: .ci/clusters/values-jwt-generated.yaml
+ shortname: jwt-generated
- name: TLS
values_file: .ci/clusters/values-tls.yaml
shortname: tls
@@ -299,6 +302,9 @@ jobs:
"jwt-asymmetric")
export EXTRA_SUPERUSERS=manager-admin
;;
+ "jwt-generated")
+ export SKIP_PREPARE_RELEASE="1"
+ ;;
"openid")
export AUTHENTICATION_PROVIDER=openid
;;
diff --git a/charts/pulsar/templates/jwt-secret-init.yaml
b/charts/pulsar/templates/jwt-secret-init.yaml
new file mode 100644
index 0000000..c4bd8b5
--- /dev/null
+++ b/charts/pulsar/templates/jwt-secret-init.yaml
@@ -0,0 +1,253 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+#
+
+# When JWT authentication is enabled and generateSecrets is true, this
pre-install/pre-upgrade
+# hook job generates JWT signing keys and per-subject tokens, storing them as
Kubernetes secrets.
+# The job is skipped entirely (not rendered) if the signing key secret already
exists.
+# Individual token secrets that already exist are not recreated, but their
annotations may be
+{{- if and .Values.auth.authentication.enabled
.Values.auth.authentication.jwt.enabled
.Values.auth.authentication.jwt.generateSecrets.enabled }}
+{{- $ns := include "pulsar.namespace" . }}
+{{- $keySecret := ternary "symmetric" "asymmetric"
.Values.auth.authentication.jwt.usingSecretKey }}
+{{- if not (lookup "v1" "Secret" $ns (printf "%s-token-%s-key" .Release.Name
$keySecret)) }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ namespace: {{ $ns }}
+ labels:
+ {{- include "pulsar.standardLabels" . | nindent 4 }}
+ component: jwt-secret-init
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy":
before-hook-creation,hook-succeeded,hook-failed
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ namespace: {{ $ns }}
+ labels:
+ {{- include "pulsar.standardLabels" . | nindent 4 }}
+ component: jwt-secret-init
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy":
before-hook-creation,hook-succeeded,hook-failed
+rules:
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "create", "patch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ namespace: {{ $ns }}
+ labels:
+ {{- include "pulsar.standardLabels" . | nindent 4 }}
+ component: jwt-secret-init
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-10"
+ "helm.sh/hook-delete-policy":
before-hook-creation,hook-succeeded,hook-failed
+subjects:
+ - kind: ServiceAccount
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+roleRef:
+ kind: Role
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ namespace: {{ $ns }}
+ labels:
+ {{- include "pulsar.standardLabels" . | nindent 4 }}
+ component: jwt-secret-init
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "0"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 1
+ template:
+ metadata:
+ labels:
+ {{- include "pulsar.template.labels" . | nindent 8 }}
+ component: jwt-secret-init
+ spec:
+ {{- include "pulsar.imagePullSecrets" . | nindent 6 }}
+ serviceAccountName: "{{ template "pulsar.fullname" . }}-jwt-secret-init"
+ restartPolicy: Never
+ {{- if .Values.auth.authentication.jwt.generateSecrets.nodeSelector }}
+ nodeSelector:
+{{ toYaml .Values.auth.authentication.jwt.generateSecrets.nodeSelector |
indent 8 }}
+ {{- end }}
+ {{- if .Values.auth.authentication.jwt.generateSecrets.tolerations }}
+ tolerations:
+{{ toYaml .Values.auth.authentication.jwt.generateSecrets.tolerations | indent
8 }}
+ {{- end }}
+ {{- if and .Values.affinity.anti_affinity
.Values.auth.authentication.jwt.generateSecrets.affinity.anti_affinity }}
+ affinity:
+ podAntiAffinity:
+ {{- if eq
.Values.auth.authentication.jwt.generateSecrets.affinity.type
"requiredDuringSchedulingIgnoredDuringExecution" }}
+ {{ .Values.auth.authentication.jwt.generateSecrets.affinity.type }}:
+ - labelSelector:
+ matchExpressions:
+ - key: "app"
+ operator: In
+ values:
+ - "{{ template "pulsar.name" . }}"
+ - key: "release"
+ operator: In
+ values:
+ - {{ .Release.Name }}
+ - key: "component"
+ operator: In
+ values:
+ - jwt-secret-init
+ topologyKey: {{
.Values.auth.authentication.jwt.generateSecrets.affinity.anti_affinity_topology_key
}}
+ {{- else }}
+ {{ .Values.auth.authentication.jwt.generateSecrets.affinity.type }}:
+ - weight: 100
+ podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: "app"
+ operator: In
+ values:
+ - "{{ template "pulsar.name" . }}"
+ - key: "release"
+ operator: In
+ values:
+ - {{ .Release.Name }}
+ - key: "component"
+ operator: In
+ values:
+ - jwt-secret-init
+ topologyKey: {{
.Values.auth.authentication.jwt.generateSecrets.affinity.anti_affinity_topology_key
}}
+ {{- end }}
+ {{- end }}
+ volumes:
+ - name: jwt-secrets
+ emptyDir:
+ sizeLimit: 1Mi
+ initContainers:
+ - name: generate-jwt-artifacts
+ image: "{{ template "pulsar.imageFullName" (dict "image"
.Values.images.broker "root" .) }}"
+ imagePullPolicy: {{ include "pulsar.imagePullPolicy" (dict "image"
.Values.images.broker "root" .) }}
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -eou pipefail
+ {{- if .Values.auth.authentication.jwt.usingSecretKey }}
+ echo "Generating symmetric secret key..."
+ bin/pulsar tokens create-secret-key --output
/jwt-secrets/secret.key
+ {{- range $role, $subject := .Values.auth.superUsers }}
+ {{- if $subject }}
+ echo "Generating token for subject '{{ $subject }}'..."
+ bin/pulsar tokens create \
+ --secret-key /jwt-secrets/secret.key \
+ --subject "{{ $subject }}" | tr -d '\n' > "/jwt-secrets/{{
$subject }}.token"
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ echo "Generating RSA key pair..."
+ bin/pulsar tokens create-key-pair \
+ -a RS256 \
+ --output-private-key=/jwt-secrets/private.key \
+ --output-public-key=/jwt-secrets/public.key
+ {{- range $role, $subject := .Values.auth.superUsers }}
+ {{- if $subject }}
+ echo "Generating token for subject '{{ $subject }}'..."
+ bin/pulsar tokens create \
+ -a RS256 \
+ --private-key=/jwt-secrets/private.key \
+ --subject "{{ $subject }}" | tr -d '\n' > "/jwt-secrets/{{
$subject }}.token"
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ echo "JWT artifact generation complete"
+ volumeMounts:
+ - mountPath: /jwt-secrets
+ name: jwt-secrets
+ containers:
+ - name: create-secrets
+ image: "{{ template "pulsar.imageFullName" (dict "image"
.Values.images.kubectl "root" .) }}"
+ imagePullPolicy: {{ include "pulsar.imagePullPolicy" (dict "image"
.Values.images.kubectl "root" .) }}
+ env:
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ command:
+ - /bin/sh
+ - -c
+ - |
+ set -e
+
+ create_secret() {
+ SECRET_NAME="$1"
+ shift
+ if output=$(kubectl create secret generic "$SECRET_NAME" -n
"$NAMESPACE" "$@" 2>&1); then
+ echo "Created secret $SECRET_NAME"
+ return 0
+ elif echo "$output" | grep -q "already exists"; then
+ echo "Secret $SECRET_NAME already exists, skipping"
+ return 0
+ else
+ echo "Failed to create secret $SECRET_NAME: $output" >&2
+ exit 1
+ fi
+ }
+
+ # Create signing key secret
+ {{- if .Values.auth.authentication.jwt.usingSecretKey }}
+ create_secret "{{ .Release.Name }}-token-symmetric-key"
--from-file="SECRETKEY=/jwt-secrets/secret.key"
+ {{- range $k, $v :=
.Values.auth.authentication.jwt.secretAnnotations.key }}
+ kubectl annotate secret "{{ $.Release.Name
}}-token-symmetric-key" -n "$NAMESPACE" --overwrite "{{ $k }}={{ $v }}"
+ {{- end }}
+ {{- else }}
+ create_secret "{{ .Release.Name }}-token-asymmetric-key"
--from-file="PRIVATEKEY=/jwt-secrets/private.key"
--from-file="PUBLICKEY=/jwt-secrets/public.key"
+ {{- range $k, $v :=
.Values.auth.authentication.jwt.secretAnnotations.key }}
+ kubectl annotate secret "{{ $.Release.Name
}}-token-asymmetric-key" -n "$NAMESPACE" --overwrite "{{ $k }}={{ $v }}"
+ {{- end }}
+ {{- end }}
+
+ # Create token secrets for each super user subject
+ {{- range $role, $subject := .Values.auth.superUsers }}
+ {{- if $subject }}
+ create_secret "{{ $.Release.Name }}-token-{{ $subject }}"
--from-file="TOKEN=/jwt-secrets/{{ $subject }}.token"
+ {{- $subjectAnnotations := (index
($.Values.auth.authentication.jwt.secretAnnotations.token | default dict)
$role) | default dict }}
+ {{- range $k, $v := $subjectAnnotations }}
+ kubectl annotate secret "{{ $.Release.Name }}-token-{{ $subject
}}" -n "$NAMESPACE" --overwrite "{{ $k }}={{ $v }}"
+ {{- end }}
+ {{- end }}
+ {{- end }}
+
+ echo "JWT secret initialization complete"
+ volumeMounts:
+ - mountPath: /jwt-secrets
+ name: jwt-secrets
+{{- end }}
+{{- end }}
diff --git a/charts/pulsar/values.yaml b/charts/pulsar/values.yaml
index 47fb6a3..b99be66 100755
--- a/charts/pulsar/values.yaml
+++ b/charts/pulsar/values.yaml
@@ -222,6 +222,8 @@ images:
kubectl:
repository: alpine/k8s
tag: 1.32.12
+ # uses defaultPullPolicy when unspecified
+ pullPolicy:
## TLS
## templates/tls-certs.yaml
@@ -347,6 +349,32 @@ auth:
# If the token is generated by a secret key, set the usingSecretKey as
true.
# If the token is generated by a private key, set the usingSecretKey as
false.
usingSecretKey: false
+ # When enabled, a pre-install/pre-upgrade hook job generates JWT signing
keys and
+ # per-subject tokens as Kubernetes secrets. Skipped if the signing key
secret already exists.
+ generateSecrets:
+ enabled: false
+ # nodeSelector:
+ # cloud.google.com/gke-nodepool: default-pool
+ tolerations: []
+ affinity:
+ anti_affinity: false
+ anti_affinity_topology_key: kubernetes.io/hostname
+ # Valid values:
+ # requiredDuringSchedulingIgnoredDuringExecution - rules must be met
for pod to be scheduled (hard)
+ # preferredDuringSchedulingIgnoredDuringExecution - scheduler will
try to enforce but not guarantee
+ type: preferredDuringSchedulingIgnoredDuringExecution
+ # Annotations to apply to secrets created by the generateSecrets job
+ secretAnnotations:
+ # Annotations added to the signing key secret (asymmetric-key or
symmetric-key)
+ key: {}
+ # Per-role annotations for token secrets. Map of auth.superUsers role
key to annotation map
+ # (for example: broker, proxy, client, manager).
+ # Example:
+ # client:
+ # reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
+ # reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
+ # broker: {}
+ token: {}
openid:
enabled: false
# #
https://pulsar.apache.org/docs/next/security-openid-connect/#enable-openid-connect-authentication-in-the-broker-and-proxy