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

Reply via email to