diff --git a/helm/templates/_jaas.tpl b/helm/templates/_jaas.tpl new file mode 100644 index 0000000000..c96aecc487 --- /dev/null +++ b/helm/templates/_jaas.tpl @@ -0,0 +1,145 @@ +{{/* +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. +*/}} + +{{/* +Renders the env entries for the JAAS init container: secretKeyRef for each +Secret-sourced credential, literal `value:` for each values-sourced one. +Usage: + include "fluss.security.jaas.initContainer.env" . +*/}} +{{- define "fluss.security.jaas.initContainer.env" -}} +{{- $internalMechanism := include "fluss.security.listener.mechanism" (dict "context" .Values "listener" "internal") -}} +{{- $clientMechanism := include "fluss.security.listener.mechanism" (dict "context" .Values "listener" "client") -}} +{{- $zkEnabled := include "fluss.security.zookeeper.sasl.enabled" . -}} +{{- /* internal */ -}} +{{- if and (include "fluss.security.sasl.plain.enabled" .) (eq $internalMechanism "plain") }} +- name: {{ include "fluss.security.sasl.plain.internal.envVarName" "username" }} +{{- $ref := .Values.security.internal.sasl.plain.existingSecret | default (dict) }} +{{- if $ref.name }} + valueFrom: + secretKeyRef: + name: {{ $ref.name }} + key: {{ $ref.usernameKey | default "username" }} +{{- else }} + value: {{ include "fluss.security.sasl.plain.internal.username" . | quote }} +{{- end }} +- name: {{ include "fluss.security.sasl.plain.internal.envVarName" "password" }} +{{- if $ref.name }} + valueFrom: + secretKeyRef: + name: {{ $ref.name }} + key: {{ $ref.passwordKey | default "password" }} +{{- else }} + value: {{ include "fluss.security.sasl.plain.internal.password" . | quote }} +{{- end }} +{{- end }} +{{- /* client */ -}} +{{- if and (include "fluss.security.sasl.plain.enabled" .) (eq $clientMechanism "plain") }} +{{- range $idx, $user := .Values.security.client.sasl.plain.users | default (list) }} +{{- $ref := $user.existingSecret | default (dict) }} +- name: {{ include "fluss.security.sasl.plain.client.envVarName" (dict "field" "username" "idx" $idx) }} +{{- if $ref.name }} + valueFrom: + secretKeyRef: + name: {{ $ref.name }} + key: {{ $ref.usernameKey | default "username" }} +{{- else }} + value: {{ $user.username | quote }} +{{- end }} +- name: {{ include "fluss.security.sasl.plain.client.envVarName" (dict "field" "password" "idx" $idx) }} +{{- if $ref.name }} + valueFrom: + secretKeyRef: + name: {{ $ref.name }} + key: {{ $ref.passwordKey | default "password" }} +{{- else }} + value: {{ $user.password | quote }} +{{- end }} +{{- end }} +{{- end }} +{{- /* zookeeper */ -}} +{{- if $zkEnabled }} +- name: {{ include "fluss.security.sasl.plain.zookeeper.envVarName" "username" }} +{{- $zkRef := .Values.security.zookeeper.sasl.plain.existingSecret | default (dict) }} +{{- if $zkRef.name }} + valueFrom: + secretKeyRef: + name: {{ $zkRef.name }} + key: {{ $zkRef.usernameKey | default "username" }} +{{- else }} + value: {{ .Values.security.zookeeper.sasl.plain.username | quote }} +{{- end }} +- name: {{ include "fluss.security.sasl.plain.zookeeper.envVarName" "password" }} +{{- if $zkRef.name }} + valueFrom: + secretKeyRef: + name: {{ $zkRef.name }} + key: {{ $zkRef.passwordKey | default "password" }} +{{- else }} + value: {{ .Values.security.zookeeper.sasl.plain.password | quote }} +{{- end }} +{{- end }} +{{- end -}} + +{{/* +Init container spec: mounts the rendered JAAS template ConfigMap at /tmpl, +resolves credential placeholders via envsubst, writes the result to the +shared emptyDir at /jaas, and chmods it 0400. +Usage: + include "fluss.security.jaas.initContainer" . +*/}} +{{- define "fluss.security.jaas.initContainer" -}} +- name: render-jaas-config + image: {{ include "fluss.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- include "fluss.security.jaas.initContainer.env" . | nindent 4 }} + command: + - /bin/sh + - -ec + - | + umask 077 + envsubst < /tmpl/jaas.conf > /jaas/jaas.conf + chmod 0400 /jaas/jaas.conf + {{- if include "fluss.security.zookeeper.sasl.enabled" . }} + cp /tmpl/zookeeper-client.properties /jaas/zookeeper-client.properties + chmod 0400 /jaas/zookeeper-client.properties + {{- end }} + volumeMounts: + - name: sasl-template + mountPath: /tmpl + readOnly: true + - name: sasl-config + mountPath: /jaas +{{- end -}} + +{{/* +Pod-level volumes for the JAAS render path: the template ConfigMap (input) +and an in-memory emptyDir (output, shared with the main container). +Usage: + include "fluss.security.jaas.volumes" . +*/}} +{{- define "fluss.security.jaas.volumes" -}} +- name: sasl-template + configMap: + name: {{ include "fluss.security.jaas.configName" . }} +- name: sasl-config + emptyDir: + medium: Memory + sizeLimit: 1Mi +{{- end -}} diff --git a/helm/templates/_security.tpl b/helm/templates/_security.tpl index f3684fcd47..35e3c29954 100644 --- a/helm/templates/_security.tpl +++ b/helm/templates/_security.tpl @@ -118,7 +118,9 @@ Usage: {{- end -}} {{/* -Validates that the client PLAIN mechanism block contains the required users. +Validates security.client.sasl.plain.users. Each entry is either a literal +{username, password} pair OR {existingSecret: {name, usernameKey?, passwordKey?}}. +Mixing the two shapes within one entry is not allowed. Returns an error message if invalid, empty string otherwise. Usage: include "fluss.security.sasl.validateClientPlainUsers" . @@ -130,11 +132,22 @@ Usage: {{- if eq (len $users) 0 -}} {{- print "security.client.sasl.plain.users must contain at least one user when security.client.sasl.mechanism is plain" -}} {{- else -}} + {{- $errs := list -}} {{- range $idx, $user := $users -}} - {{- if or (empty $user.username) (empty $user.password) -}} - {{- printf "security.client.sasl.plain.users[%d] must set both username and password" $idx -}} + {{- $ref := $user.existingSecret | default (dict) -}} + {{- $hasLiteral := or (not (empty $user.username)) (not (empty $user.password)) -}} + {{- $hasRef := not (empty $ref.name) -}} + {{- if and $hasLiteral $hasRef -}} + {{- $errs = append $errs (printf "security.client.sasl.plain.users[%d] cannot set username/password and existingSecret" $idx) -}} + {{- else if $hasRef -}} + {{/* existingSecret path — name is the only required field */}} + {{- else -}} + {{- if or (empty $user.username) (empty $user.password) -}} + {{- $errs = append $errs (printf "security.client.sasl.plain.users[%d] must set both username and password (or existingSecret)" $idx) -}} + {{- end -}} {{- end -}} {{- end -}} + {{- join "\n" $errs -}} {{- end -}} {{- end -}} {{- end -}} @@ -166,26 +179,36 @@ Usage: {{- end -}} {{/* -Validates that ZooKeeper SASL username is not empty when ZK SASL is enabled. +Validates that ZooKeeper SASL username is provided when ZK SASL is enabled, +either as a literal or via existingSecret. Returns an error message if invalid, empty string otherwise. Usage: include "fluss.security.zookeeper.sasl.validateUsername" . */}} {{- define "fluss.security.zookeeper.sasl.validateUsername" -}} -{{- if and (include "fluss.security.zookeeper.sasl.enabled" .) (not .Values.security.zookeeper.sasl.plain.username) -}} - {{- print "security.zookeeper.sasl.plain.username must not be empty when security.zookeeper.sasl.mechanism is plain" -}} +{{- if include "fluss.security.zookeeper.sasl.enabled" . -}} + {{- if not (include "fluss.security.sasl.plain.zookeeper.fromSecret" .) -}} + {{- if not .Values.security.zookeeper.sasl.plain.username -}} + {{- print "security.zookeeper.sasl.plain.username must not be empty when security.zookeeper.sasl.mechanism is plain (or set security.zookeeper.sasl.plain.existingSecret)" -}} + {{- end -}} + {{- end -}} {{- end -}} {{- end -}} {{/* -Validates that ZooKeeper SASL password is not empty when ZK SASL is enabled. +Validates that ZooKeeper SASL password is provided when ZK SASL is enabled, +either as a literal or via existingSecret. Returns an error message if invalid, empty string otherwise. Usage: include "fluss.security.zookeeper.sasl.validatePassword" . */}} {{- define "fluss.security.zookeeper.sasl.validatePassword" -}} -{{- if and (include "fluss.security.zookeeper.sasl.enabled" .) (not .Values.security.zookeeper.sasl.plain.password) -}} - {{- print "security.zookeeper.sasl.plain.password must not be empty when security.zookeeper.sasl.mechanism is plain" -}} +{{- if include "fluss.security.zookeeper.sasl.enabled" . -}} + {{- if not (include "fluss.security.sasl.plain.zookeeper.fromSecret" .) -}} + {{- if not .Values.security.zookeeper.sasl.plain.password -}} + {{- print "security.zookeeper.sasl.plain.password must not be empty when security.zookeeper.sasl.mechanism is plain (or set security.zookeeper.sasl.plain.existingSecret)" -}} + {{- end -}} + {{- end -}} {{- end -}} {{- end -}} @@ -245,8 +268,10 @@ Usage: {{- if (include "fluss.security.sasl.enabled" .) -}} {{- $internalMechanism := include "fluss.security.listener.mechanism" (dict "context" .Values "listener" "internal") -}} {{- if eq $internalMechanism "plain" -}} - {{- if and (not .Values.security.internal.sasl.plain.username) (not .Values.security.internal.sasl.plain.password) -}} - {{- print "You are using AUTO-GENERATED SASL credentials for internal communication.\n It is strongly recommended to set the following values in production:\n - security.internal.sasl.plain.username\n - security.internal.sasl.plain.password" -}} + {{- if not (include "fluss.security.sasl.plain.internal.fromSecret" .) -}} + {{- if and (not .Values.security.internal.sasl.plain.username) (not .Values.security.internal.sasl.plain.password) -}} + {{- print "You are using AUTO-GENERATED SASL credentials for internal communication.\n It is strongly recommended to set the following values in production:\n - security.internal.sasl.plain.username\n - security.internal.sasl.plain.password\n Or source from an existing Secret via security.internal.sasl.plain.existingSecret" -}} + {{- end -}} {{- end -}} {{- end -}} {{- end -}} @@ -294,3 +319,54 @@ Usage: {{- define "fluss.security.jaas.configName" -}} {{ include "fluss.fullname" . }}-sasl-jaas-config {{- end -}} + +{{/* +Returns "true" if internal SASL credentials come from an existingSecret. +Usage: + include "fluss.security.sasl.plain.internal.fromSecret" . +*/}} +{{- define "fluss.security.sasl.plain.internal.fromSecret" -}} +{{- $ref := .Values.security.internal.sasl.plain.existingSecret | default (dict) -}} +{{- if $ref.name -}}true{{- end -}} +{{- end -}} + +{{/* +Returns "true" if ZooKeeper SASL credentials come from an existingSecret. +Usage: + include "fluss.security.sasl.plain.zookeeper.fromSecret" . +*/}} +{{- define "fluss.security.sasl.plain.zookeeper.fromSecret" -}} +{{- $ref := .Values.security.zookeeper.sasl.plain.existingSecret | default (dict) -}} +{{- if $ref.name -}}true{{- end -}} +{{- end -}} + +{{/* +Returns the env-var name for an internal SASL credential field. +Usage: + include "fluss.security.sasl.plain.internal.envVarName" "username" + include "fluss.security.sasl.plain.internal.envVarName" "password" +*/}} +{{- define "fluss.security.sasl.plain.internal.envVarName" -}} +{{- printf "FLUSS_JAAS_INTERNAL_%s" (upper .) -}} +{{- end -}} + +{{/* +Returns the env-var name for a ZooKeeper SASL credential field. +Usage: + include "fluss.security.sasl.plain.zookeeper.envVarName" "username" + include "fluss.security.sasl.plain.zookeeper.envVarName" "password" +*/}} +{{- define "fluss.security.sasl.plain.zookeeper.envVarName" -}} +{{- printf "FLUSS_JAAS_ZOOKEEPER_%s" (upper .) -}} +{{- end -}} + +{{/* +Returns the env-var name for a client user credential field at a given index. +Usage: + include "fluss.security.sasl.plain.client.envVarName" (dict "field" "username" "idx" 2) + => FLUSS_JAAS_CLIENT_USERNAME_2 +*/}} +{{- define "fluss.security.sasl.plain.client.envVarName" -}} +{{- printf "FLUSS_JAAS_CLIENT_%s_%d" (upper .field) (int .idx) -}} +{{- end -}} + diff --git a/helm/templates/secret-jaas-config.yaml b/helm/templates/configmap-jaas-template.yaml similarity index 77% rename from helm/templates/secret-jaas-config.yaml rename to helm/templates/configmap-jaas-template.yaml index 436a2fbb28..68b0329067 100644 --- a/helm/templates/secret-jaas-config.yaml +++ b/helm/templates/configmap-jaas-template.yaml @@ -19,34 +19,31 @@ {{ if (include "fluss.security.jaas.required" .) }} {{- $internalMechanism := include "fluss.security.listener.mechanism" (dict "context" .Values "listener" "internal") -}} {{- $clientMechanism := include "fluss.security.listener.mechanism" (dict "context" .Values "listener" "client") -}} -{{- $internalUsername := include "fluss.security.sasl.plain.internal.username" . -}} -{{- $internalPassword := include "fluss.security.sasl.plain.internal.password" . -}} apiVersion: v1 -kind: Secret +kind: ConfigMap metadata: name: {{ include "fluss.security.jaas.configName" . }} labels: {{- include "fluss.labels" . | nindent 4 }} -type: Opaque -stringData: +data: jaas.conf: | {{- if (include "fluss.security.sasl.plain.enabled" .) }} {{- if eq $internalMechanism "plain" }} internal.FlussServer { org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required - user_{{ $internalUsername }}="{{ $internalPassword }}"; + user_${FLUSS_JAAS_INTERNAL_USERNAME}="${FLUSS_JAAS_INTERNAL_PASSWORD}"; }; FlussClient { org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required - username="{{ $internalUsername }}" - password="{{ $internalPassword }}"; + username="${FLUSS_JAAS_INTERNAL_USERNAME}" + password="${FLUSS_JAAS_INTERNAL_PASSWORD}"; }; {{- end }} {{- if eq $clientMechanism "plain" }} client.FlussServer { org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required -{{- range .Values.security.client.sasl.plain.users | default (list) }} - user_{{ .username }}="{{ .password }}" +{{- range $idx, $user := .Values.security.client.sasl.plain.users | default (list) }} + user_${FLUSS_JAAS_CLIENT_USERNAME_{{ $idx }}}="${FLUSS_JAAS_CLIENT_PASSWORD_{{ $idx }}}" {{- end }}; }; {{- end }} @@ -54,8 +51,8 @@ stringData: {{- if (include "fluss.security.zookeeper.sasl.enabled" .) }} ZookeeperClient { {{ .Values.security.zookeeper.sasl.plain.loginModuleClass }} required - username="{{ .Values.security.zookeeper.sasl.plain.username }}" - password="{{ .Values.security.zookeeper.sasl.plain.password }}"; + username="${FLUSS_JAAS_ZOOKEEPER_USERNAME}" + password="${FLUSS_JAAS_ZOOKEEPER_PASSWORD}"; }; {{- end }} {{- if (include "fluss.security.zookeeper.sasl.enabled" .) }} diff --git a/helm/templates/sts-coordinator.yaml b/helm/templates/sts-coordinator.yaml index 6288929a0b..918bf2d06a 100644 --- a/helm/templates/sts-coordinator.yaml +++ b/helm/templates/sts-coordinator.yaml @@ -62,9 +62,14 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.coordinator.initContainers }} + {{- if or (include "fluss.security.jaas.required" .) .Values.coordinator.initContainers }} initContainers: + {{- if include "fluss.security.jaas.required" . }} + {{- include "fluss.security.jaas.initContainer" . | nindent 8 }} + {{- end }} + {{- with .Values.coordinator.initContainers }} {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} containers: - name: {{ .Chart.Name }}-coordinator @@ -156,9 +161,7 @@ spec: emptyDir: {} {{- end }} {{- if (include "fluss.security.jaas.required" .) }} - - name: sasl-config - secret: - secretName: {{ include "fluss.security.jaas.configName" . }} + {{- include "fluss.security.jaas.volumes" . | nindent 8 }} {{- end }} {{- with .Values.coordinator.extraVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml index 60abb11005..6232544de3 100644 --- a/helm/templates/sts-tablet.yaml +++ b/helm/templates/sts-tablet.yaml @@ -62,9 +62,14 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.tablet.initContainers }} + {{- if or (include "fluss.security.jaas.required" .) .Values.tablet.initContainers }} initContainers: + {{- if include "fluss.security.jaas.required" . }} + {{- include "fluss.security.jaas.initContainer" . | nindent 8 }} + {{- end }} + {{- with .Values.tablet.initContainers }} {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} containers: - name: {{ .Chart.Name }}-tablet @@ -153,9 +158,7 @@ spec: emptyDir: {} {{- end }} {{- if (include "fluss.security.jaas.required" .) }} - - name: sasl-config - secret: - secretName: {{ include "fluss.security.jaas.configName" . }} + {{- include "fluss.security.jaas.volumes" . | nindent 8 }} {{- end }} {{- with .Values.tablet.extraVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm/tests/sasl_existing_secret_test.yaml b/helm/tests/sasl_existing_secret_test.yaml new file mode 100644 index 0000000000..6d0c3671bc --- /dev/null +++ b/helm/tests/sasl_existing_secret_test.yaml @@ -0,0 +1,316 @@ +# +# 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. +# + +suite: sasl-existing-secret +templates: + - templates/sts-coordinator.yaml + - templates/sts-tablet.yaml + - templates/configmap-jaas-template.yaml + +tests: + - it: renders init container and ConfigMap template when SASL is enabled + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: u + security.internal.sasl.plain.password: p + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: render-jaas-config + template: templates/sts-coordinator.yaml + - equal: + path: spec.template.spec.initContainers[0].name + value: render-jaas-config + template: templates/sts-tablet.yaml + - containsDocument: + kind: ConfigMap + apiVersion: v1 + template: templates/configmap-jaas-template.yaml + + - it: does not render init container or ConfigMap when SASL is disabled + set: {} + asserts: + - isNull: + path: spec.template.spec.initContainers + template: templates/sts-coordinator.yaml + - hasDocuments: + count: 0 + template: templates/configmap-jaas-template.yaml + + - it: template contains envsubst placeholders, not literal credentials + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: leak-if-visible + security.internal.sasl.plain.password: leak-if-visible-pass + asserts: + - matchRegex: + path: data["jaas.conf"] + pattern: 'user_\$\{FLUSS_JAAS_INTERNAL_USERNAME\}="\$\{FLUSS_JAAS_INTERNAL_PASSWORD\}"' + template: templates/configmap-jaas-template.yaml + - notMatchRegex: + path: data["jaas.conf"] + pattern: 'leak-if-visible' + template: templates/configmap-jaas-template.yaml + + - it: literal internal credentials flow via env.value + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: alice + security.internal.sasl.plain.password: alice-pass + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_USERNAME + value: alice + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_PASSWORD + value: alice-pass + template: templates/sts-coordinator.yaml + + - it: internal credentials from existingSecret flow via secretKeyRef + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.existingSecret.name: fluss-internal-sasl + security.internal.sasl.plain.existingSecret.usernameKey: u + security.internal.sasl.plain.existingSecret.passwordKey: p + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_USERNAME + valueFrom: + secretKeyRef: + name: fluss-internal-sasl + key: u + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_PASSWORD + valueFrom: + secretKeyRef: + name: fluss-internal-sasl + key: p + template: templates/sts-coordinator.yaml + + - it: internal existingSecret keys default to username/password + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.existingSecret.name: fluss-internal-sasl + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_USERNAME + valueFrom: + secretKeyRef: + name: fluss-internal-sasl + key: username + template: templates/sts-coordinator.yaml + + - it: zookeeper credentials from existingSecret + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.existingSecret.name: fluss-zk-sasl + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_ZOOKEEPER_USERNAME + valueFrom: + secretKeyRef: + name: fluss-zk-sasl + key: username + template: templates/sts-tablet.yaml + + - it: client user with literal pair uses env.value for both fields + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - username: alice + password: alice-literal + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_CLIENT_USERNAME_0 + value: alice + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_CLIENT_PASSWORD_0 + value: alice-literal + template: templates/sts-coordinator.yaml + + - it: client user with existingSecret sources both fields via secretKeyRef + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - existingSecret: + name: fluss-client-sasl-bob + usernameKey: u + passwordKey: p + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_CLIENT_USERNAME_0 + valueFrom: + secretKeyRef: + name: fluss-client-sasl-bob + key: u + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_CLIENT_PASSWORD_0 + valueFrom: + secretKeyRef: + name: fluss-client-sasl-bob + key: p + template: templates/sts-coordinator.yaml + + - it: client user existingSecret keys default to username/password + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - existingSecret: + name: fluss-client-sasl-bob + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_CLIENT_USERNAME_0 + valueFrom: + secretKeyRef: + name: fluss-client-sasl-bob + key: username + template: templates/sts-coordinator.yaml + + - it: ConfigMap template uses username placeholder, not a literal name + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - username: alice + password: alice-pass + - existingSecret: + name: fluss-client-sasl-bob + asserts: + - matchRegex: + path: data["jaas.conf"] + pattern: 'user_\$\{FLUSS_JAAS_CLIENT_USERNAME_0\}="\$\{FLUSS_JAAS_CLIENT_PASSWORD_0\}"' + template: templates/configmap-jaas-template.yaml + - matchRegex: + path: data["jaas.conf"] + pattern: 'user_\$\{FLUSS_JAAS_CLIENT_USERNAME_1\}="\$\{FLUSS_JAAS_CLIENT_PASSWORD_1\}";' + template: templates/configmap-jaas-template.yaml + + - it: init container command runs envsubst + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: u + security.internal.sasl.plain.password: p + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'envsubst < /tmpl/jaas.conf > /jaas/jaas.conf' + template: templates/sts-coordinator.yaml + + - it: init container mounts template ConfigMap read-only and writable emptyDir + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: u + security.internal.sasl.plain.password: p + asserts: + - contains: + path: spec.template.spec.initContainers[0].volumeMounts + content: + name: sasl-template + mountPath: /tmpl + readOnly: true + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.initContainers[0].volumeMounts + content: + name: sasl-config + mountPath: /jaas + template: templates/sts-coordinator.yaml + - contains: + path: spec.template.spec.volumes + content: + name: sasl-config + emptyDir: + medium: Memory + sizeLimit: 1Mi + template: templates/sts-coordinator.yaml + + - it: combines render-jaas-config init container with user-provided ones + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: u + security.internal.sasl.plain.password: p + coordinator.initContainers: + - name: wait-for-zk + image: busybox + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: render-jaas-config + template: templates/sts-coordinator.yaml + - equal: + path: spec.template.spec.initContainers[1].name + value: wait-for-zk + template: templates/sts-coordinator.yaml +--- +suite: sasl-existing-secret-validation +templates: + - templates/NOTES.txt +tests: + - it: fails when client user mixes literal fields and existingSecret + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - username: alice + existingSecret: + name: fluss-client-sasl-alice + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nsecurity.client.sasl.plain.users[0] cannot set username/password and existingSecret" + + - it: fails when a literal client user is missing password + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - username: alice + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nsecurity.client.sasl.plain.users[0] must set both username and password (or existingSecret)" + + - it: fails when a literal client user is missing username + set: + security.client.sasl.mechanism: plain + security.client.sasl.plain.users: + - password: alice-pass + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nsecurity.client.sasl.plain.users[0] must set both username and password (or existingSecret)" diff --git a/helm/tests/security_test.yaml b/helm/tests/security_test.yaml index ec33b9d739..e5d624a68b 100644 --- a/helm/tests/security_test.yaml +++ b/helm/tests/security_test.yaml @@ -18,11 +18,11 @@ suite: plain-sasl-config templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml - templates/configmap.yaml tests: - it: renders JAAS secret and protocol map for plain mechanisms - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml set: security.client.sasl.mechanism: plain security.client.sasl.plain.users: @@ -35,10 +35,10 @@ tests: - hasDocuments: count: 1 - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'internal\.FlussServer\s*\{' - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'client\.FlussServer\s*\{' - it: writes SASL protocol map and enabled mechanisms in configmap template: templates/configmap.yaml @@ -76,34 +76,31 @@ tests: suite: plain-internal-defaults templates: - - templates/secret-jaas-config.yaml + - templates/sts-coordinator.yaml tests: - it: uses auto-generated internal credentials when user does not override them set: security.internal.sasl.mechanism: plain asserts: - - hasDocuments: - count: 1 - - matchRegex: - path: stringData["jaas.conf"] - pattern: 'user_fluss-internal-user-RELEASE-NAME="[a-f0-9]{64}"' - - matchRegex: - path: stringData["jaas.conf"] - pattern: 'username="fluss-internal-user-RELEASE-NAME"' + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: FLUSS_JAAS_INTERNAL_USERNAME + value: fluss-internal-user-RELEASE-NAME - matchRegex: - path: stringData["jaas.conf"] - pattern: 'password="[a-f0-9]{64}"' + path: spec.template.spec.initContainers[0].env[?(@.name=="FLUSS_JAAS_INTERNAL_PASSWORD")].value + pattern: '^[a-f0-9]{64}$' --- suite: security-disabled-defaults templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml - templates/sts-coordinator.yaml - templates/configmap.yaml tests: - it: does not render JAAS secret by default - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml asserts: - hasDocuments: count: 0 @@ -128,12 +125,12 @@ tests: suite: plain-internal-only templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml - templates/sts-coordinator.yaml - templates/configmap.yaml tests: - it: renders internal server and client JAAS blocks only - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml set: security.internal.sasl.mechanism: plain security.internal.sasl.plain.username: internal-user @@ -142,13 +139,13 @@ tests: - hasDocuments: count: 1 - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'internal\.FlussServer\s*\{' - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'FlussClient\s*\{' - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'client\.FlussServer\s*\{' - it: renders security config in configmap for internal SASL template: templates/configmap.yaml @@ -181,23 +178,30 @@ tests: content: name: FLUSS_ENV_JAVA_OPTS value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" + - contains: + path: spec.template.spec.volumes + content: + name: sasl-template + configMap: + name: RELEASE-NAME-fluss-sasl-jaas-config - contains: path: spec.template.spec.volumes content: name: sasl-config - secret: - secretName: RELEASE-NAME-fluss-sasl-jaas-config + emptyDir: + medium: Memory + sizeLimit: 1Mi --- suite: plain-client-only templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml - templates/sts-coordinator.yaml - templates/configmap.yaml tests: - it: renders client JAAS block only - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml set: security.client.sasl.mechanism: plain security.client.sasl.plain.users: @@ -207,13 +211,13 @@ tests: - hasDocuments: count: 1 - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'client\.FlussServer\s*\{' - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'internal\.FlussServer\s*\{' - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'FlussClient\s*\{' - it: does not render internal client sasl config for coordinator when internal listener is plaintext template: templates/configmap.yaml @@ -287,7 +291,7 @@ tests: suite: zookeeper-sasl-enabled-secret templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml tests: - it: renders ZookeeperClient JAAS block when ZK SASL is enabled set: @@ -298,19 +302,19 @@ tests: - hasDocuments: count: 1 - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'ZookeeperClient\s*\{' - matchRegex: - path: stringData["jaas.conf"] - pattern: 'username="zk-user"' + path: data["jaas.conf"] + pattern: 'username="\$\{FLUSS_JAAS_ZOOKEEPER_USERNAME\}"' - matchRegex: - path: stringData["jaas.conf"] - pattern: 'password="zk-pass"' + path: data["jaas.conf"] + pattern: 'password="\$\{FLUSS_JAAS_ZOOKEEPER_PASSWORD\}"' - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'DigestLoginModule required' - matchRegex: - path: stringData["zookeeper-client.properties"] + path: data["zookeeper-client.properties"] pattern: 'zookeeper\.sasl\.clientconfig=ZookeeperClient' - it: renders both SASL and ZK JAAS blocks when both are enabled set: @@ -322,10 +326,10 @@ tests: security.zookeeper.sasl.plain.password: zk-pass asserts: - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'internal\.FlussServer\s*\{' - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'ZookeeperClient\s*\{' - it: does not render SASL listener blocks when only ZK SASL is enabled set: @@ -334,10 +338,10 @@ tests: security.zookeeper.sasl.plain.password: zk-pass asserts: - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'internal\.FlussServer\s*\{' - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'FlussClient\s*\{' --- @@ -360,12 +364,19 @@ tests: content: name: FLUSS_ENV_JAVA_OPTS value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" + - contains: + path: spec.template.spec.volumes + content: + name: sasl-template + configMap: + name: RELEASE-NAME-fluss-sasl-jaas-config - contains: path: spec.template.spec.volumes content: name: sasl-config - secret: - secretName: RELEASE-NAME-fluss-sasl-jaas-config + emptyDir: + medium: Memory + sizeLimit: 1Mi - it: renders JAAS env var and mounts secret volume for tablet when ZK SASL is enabled template: templates/sts-tablet.yaml set: @@ -378,12 +389,19 @@ tests: content: name: FLUSS_ENV_JAVA_OPTS value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" + - contains: + path: spec.template.spec.volumes + content: + name: sasl-template + configMap: + name: RELEASE-NAME-fluss-sasl-jaas-config - contains: path: spec.template.spec.volumes content: name: sasl-config - secret: - secretName: RELEASE-NAME-fluss-sasl-jaas-config + emptyDir: + medium: Memory + sizeLimit: 1Mi - it: renders zookeeper client config path in configmap when ZK SASL is enabled template: templates/configmap.yaml set: @@ -399,7 +417,7 @@ tests: suite: zookeeper-sasl-custom-login-module templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml tests: - it: renders custom login module class in ZookeeperClient JAAS block set: @@ -409,23 +427,23 @@ tests: security.zookeeper.sasl.plain.loginModuleClass: "com.example.CustomLoginModule" asserts: - matchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'com\.example\.CustomLoginModule required' - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'DigestLoginModule' --- suite: zookeeper-sasl-disabled templates: - - templates/secret-jaas-config.yaml + - templates/configmap-jaas-template.yaml - templates/sts-coordinator.yaml - templates/sts-tablet.yaml - templates/configmap.yaml tests: - it: does not render ZookeeperClient JAAS block when ZK SASL is disabled - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml set: security.internal.sasl.mechanism: plain security.internal.sasl.plain.username: internal-user @@ -434,12 +452,12 @@ tests: - hasDocuments: count: 1 - notMatchRegex: - path: stringData["jaas.conf"] + path: data["jaas.conf"] pattern: 'ZookeeperClient\s*\{' - isNull: - path: stringData["zookeeper-client.properties"] + path: data["zookeeper-client.properties"] - it: does not render JAAS secret when neither SASL nor ZK SASL is enabled - template: templates/secret-jaas-config.yaml + template: templates/configmap-jaas-template.yaml asserts: - hasDocuments: count: 0 @@ -497,14 +515,14 @@ tests: security.zookeeper.sasl.plain.password: zk-pass asserts: - failedTemplate: - errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.plain.username must not be empty when security.zookeeper.sasl.mechanism is plain" + errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.plain.username must not be empty when security.zookeeper.sasl.mechanism is plain (or set security.zookeeper.sasl.plain.existingSecret)" - it: fails when ZK SASL is enabled but password is empty set: security.zookeeper.sasl.mechanism: plain security.zookeeper.sasl.plain.username: zk-user asserts: - failedTemplate: - errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.plain.password must not be empty when security.zookeeper.sasl.mechanism is plain" + errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.plain.password must not be empty when security.zookeeper.sasl.mechanism is plain (or set security.zookeeper.sasl.plain.existingSecret)" - it: fails for invalid zookeeper mechanism value set: security.zookeeper.sasl.mechanism: bogus diff --git a/helm/values.yaml b/helm/values.yaml index 38a0e8cf7a..55cf5be099 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -116,7 +116,16 @@ security: # "" | plain mechanism: "" plain: + # Each entry is either a literal {username, password} pair or an + # `existingSecret` reference that sources both from a Secret (same shape + # as internal/zookeeper). Mixing within one entry is not allowed. users: [] + # - username: alice + # password: alice-pass + # - existingSecret: + # name: fluss-client-sasl-bob + # usernameKey: username + # passwordKey: password internal: sasl: @@ -125,6 +134,12 @@ security: plain: username: "" password: "" + # When set, overrides literal username/password above. + existingSecret: {} + # existingSecret: + # name: fluss-internal-sasl + # usernameKey: username + # passwordKey: password zookeeper: sasl: @@ -134,6 +149,16 @@ security: username: "" password: "" loginModuleClass: "org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule" + # When set, overrides literal username/password above. + existingSecret: {} + # existingSecret: + # name: fluss-zk-sasl + # usernameKey: username + # passwordKey: password + + # JAAS config is rendered at pod startup by an init container that reads a + # template ConfigMap and substitutes credentials from env. The init container + # reuses the main Fluss image (already available on the node). metrics: reporters: "" diff --git a/website/docs/install-deploy/deploying-with-helm.md b/website/docs/install-deploy/deploying-with-helm.md index d319f380ae..32c4e03ce7 100644 --- a/website/docs/install-deploy/deploying-with-helm.md +++ b/website/docs/install-deploy/deploying-with-helm.md @@ -192,6 +192,7 @@ The following table lists the configurable parameters of the Fluss chart, and th | `security.client.sasl.plain.users` | Client listener username and password pairs for PLAIN | `[]` | | `security.internal.sasl.plain.username` | Internal listener PLAIN username | `""` | | `security.internal.sasl.plain.password` | Internal listener PLAIN password | `""` | +| `security.internal.sasl.plain.existingSecret` | Reference to a pre-existing Secret for internal SASL credentials | `{}` | Only `plain` mechanism is supported for now. An empty string disables the SASL authentication, and maps to the `PLAINTEXT` protocol. @@ -210,6 +211,154 @@ It is recommended to set these explicitly in production. | `security.zookeeper.sasl.plain.username` | ZooKeeper SASL username | `""` | | `security.zookeeper.sasl.plain.password` | ZooKeeper SASL password | `""` | | `security.zookeeper.sasl.plain.loginModuleClass` | JAAS login module class for ZooKeeper | `org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule` | +| `security.zookeeper.sasl.plain.existingSecret` | Reference to a pre-existing Secret for ZooKeeper SASL credentials | `{}` | + +#### Sourcing SASL Credentials from a Pre-existing Secret + +To keep SASL passwords out of `values.yaml` and the Helm release storage, reference a Secret managed separately — e.g., via External Secrets Operator, Sealed Secrets, or a CI pipeline. + +For internal and ZooKeeper listeners, set `existingSecret` on the listener: + +```yaml +security: + internal: + sasl: + mechanism: plain + plain: + existingSecret: + name: fluss-internal-sasl # required + usernameKey: username # optional, defaults to "username" + passwordKey: password # optional, defaults to "password" + zookeeper: + sasl: + mechanism: plain + plain: + existingSecret: + name: fluss-zk-sasl +``` + +Client users follow the same shape as internal/ZooKeeper listeners: each entry is either a literal `{username, password}` pair or an `existingSecret` reference that sources both fields from a Secret. + +```yaml +security: + client: + sasl: + mechanism: plain + plain: + users: + - username: alice + password: alice-literal-password # literal — visible in values.yaml + - existingSecret: # or resolved at pod startup + name: fluss-client-sasl-bob + usernameKey: username # optional, defaults to "username" + passwordKey: password # optional, defaults to "password" +``` + +Whenever JAAS is required, the chart renders a ConfigMap (`-fluss-sasl-jaas-config`) containing a `jaas.conf` *template* with `${FLUSS_JAAS_…}` placeholders — no credentials. An init container mounts that template, runs `envsubst` with credentials supplied via env vars (either literal `value:` entries from `values.yaml` or `valueFrom.secretKeyRef` to a pre-existing Secret), and writes the resolved `jaas.conf` to an in-memory `emptyDir` that the main Fluss container reads. + +- Literal and Secret-sourced credentials can be mixed across listeners. +- When every credential comes from a Secret, no plaintext password lives in the Helm release. +- The init container reuses the main Fluss image (already present on the node), keeping zero extra image dependencies. + +##### Example: External Secrets Operator + +If you use [External Secrets Operator](https://external-secrets.io) to sync credentials from an upstream secret manager (AWS Secrets Manager, Vault, GCP Secret Manager, etc.), the flow is: upstream → `ExternalSecret` CR → a Kubernetes `Secret` → the chart. + +For internal listener credentials stored at `prod/fluss/internal` in AWS Secrets Manager with fields `username` and `password`: + +```yaml +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fluss-internal-sasl +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secretsmanager + kind: SecretStore + target: + name: fluss-internal-sasl + data: + - secretKey: username + remoteRef: + key: prod/fluss/internal + property: username + - secretKey: password + remoteRef: + key: prod/fluss/internal + property: password +``` + +Then in `values.yaml`: + +```yaml +security: + internal: + sasl: + mechanism: plain + plain: + existingSecret: + name: fluss-internal-sasl +``` + +For the multi-user client listener, provision one Secret per user with `username` and `password` keys: + +```yaml +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fluss-client-sasl-alice +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secretsmanager + kind: SecretStore + target: + name: fluss-client-sasl-alice + data: + - secretKey: username + remoteRef: + key: prod/fluss/clients/alice + property: username + - secretKey: password + remoteRef: + key: prod/fluss/clients/alice + property: password +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fluss-client-sasl-bob +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secretsmanager + kind: SecretStore + target: + name: fluss-client-sasl-bob + data: + - secretKey: username + remoteRef: + key: prod/fluss/clients/bob + property: username + - secretKey: password + remoteRef: + key: prod/fluss/clients/bob + property: password +``` + +```yaml +security: + client: + sasl: + mechanism: plain + plain: + users: + - existingSecret: { name: fluss-client-sasl-alice } + - existingSecret: { name: fluss-client-sasl-bob } +``` + +The same pattern works with Sealed Secrets, HashiCorp Vault Agent Injector (producing a native Secret), or any other controller that lands credentials in a `Secret` — the chart only cares about the final `Secret`, not how it got there. ### Metrics Parameters