This is an automated email from the ASF dual-hosted git repository.

hongshun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new f7e4498dd [helm] Enable SASL authentication configurations (#2506)
f7e4498dd is described below

commit f7e4498dd496886b956898fcbd792fc6c025f4cd
Author: Muhammet Orazov <[email protected]>
AuthorDate: Mon Mar 16 08:00:51 2026 +0100

    [helm] Enable SASL authentication configurations (#2506)
---
 helm/README.md                                     |  32 ++-
 helm/templates/{configmap.yaml => NOTES.txt}       |  16 +-
 helm/templates/_security.tpl                       | 209 +++++++++++++++
 helm/templates/configmap.yaml                      |  17 +-
 helm/templates/secret-jaas-config.yaml             |  52 ++++
 helm/templates/sts-coordinator.yaml                |  16 +-
 helm/templates/sts-tablet.yaml                     |  14 +
 helm/tests/security_test.yaml                      | 284 +++++++++++++++++++++
 helm/values.yaml                                   |  17 ++
 website/docs/install-deploy/deploying-with-helm.md |  63 ++++-
 10 files changed, 703 insertions(+), 17 deletions(-)

diff --git a/helm/README.md b/helm/README.md
index cd0985f6e..ebc4da8f3 100644
--- a/helm/README.md
+++ b/helm/README.md
@@ -22,7 +22,7 @@ This chart deploys an Apache Fluss cluster on Kubernetes, 
following Helm best pr
 It requires a Zookeeper ensemble to be running in the same Kubernetes cluster. 
In future releases, we may add support for an embedded Zookeeper cluster.
 
 
-## Development environment 
+## Development environment
 
 | component                                                                    
  | version |
 | 
------------------------------------------------------------------------------ 
| ------- |
@@ -33,7 +33,7 @@ It requires a Zookeeper ensemble to be running in the same 
Kubernetes cluster. I
 | [Apache Fluss](https://fluss.apache.org/docs/)                               
  | v0.10.0-incubating  |
 
 
-## Image requirements 
+## Image requirements
 
 A container image for Fluss is available on DockerHub as `fluss/fluss`. You 
can use it directly or build your own from this repo. To use your own image you 
need to build the project with 
[Maven](https://fluss.apache.org/community/dev/building/) and build it with 
Docker.
 
@@ -98,6 +98,34 @@ Important Fluss options surfaced by the chart:
 - zookeeper.address must point to a reachable ensemble.
 - data.dir defaults to /tmp/fluss/data; use a PVC if persistence.enabled=true.
 
+### Security configuration
+
+The authentication methods are configured through `security` values block.
+
+```yaml
+listeners:
+  internal:
+    port: 9123
+  client:
+    port: 9124
+
+security:
+  client:
+    sasl:
+      mechanism: plain
+      plain:
+        users:
+          - username: client-user
+            password: client-password
+
+  internal:
+    sasl:
+      mechanism: plain
+      plain:
+        username: internal-user
+        password: internal-password
+```
+
 ### Private Docker Registry
 
 If you are pulling the Fluss image from a private Docker registry, you can 
configure the chart using `image.registry` and `image.pullSecrets`.
diff --git a/helm/templates/configmap.yaml b/helm/templates/NOTES.txt
similarity index 74%
copy from helm/templates/configmap.yaml
copy to helm/templates/NOTES.txt
index b0e868c90..d7a4af26f 100644
--- a/helm/templates/configmap.yaml
+++ b/helm/templates/NOTES.txt
@@ -16,14 +16,8 @@
 # limitations under the License.
 #
 
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: fluss-conf-file
-  labels:
-  {{- include "fluss.labels" . | nindent 4 }}
-data:
-  server.yaml: |
-    {{- range $key, $val := .Values.configurationOverrides }}
-    {{ $key }}: {{ tpl (printf "%v" $val) $ }}
-    {{- end }}
\ No newline at end of file
+CHART NAME: {{ .Chart.Name }}
+CHART VERSION: {{ .Chart.Version }}
+APP VERSION: {{ .Chart.AppVersion }}
+
+{{ include "fluss.security.validateValues" . }}
diff --git a/helm/templates/_security.tpl b/helm/templates/_security.tpl
new file mode 100644
index 000000000..210a35746
--- /dev/null
+++ b/helm/templates/_security.tpl
@@ -0,0 +1,209 @@
+#
+# 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.
+#
+
+{{/*
+Returns the authentication mechanism value of a given listener.
+Allowed mechanism values: '', 'plain'
+Usage:
+  include "fluss.security.listener.mechanism" (dict "context" .Values 
"listener" "client")
+*/}}
+{{- define "fluss.security.listener.mechanism" -}}
+{{- $listener := index .context.security .listener | default (dict) -}}
+{{- $sasl := $listener.sasl | default (dict) -}}
+{{- $mechanism := lower (default "" $sasl.mechanism) -}}
+{{- $mechanism -}}
+{{- end -}}
+
+{{/*
+Returns true if any of the listeners uses SASL based authentication mechanism 
('plain' for now).
+Usage:
+  include "fluss.security.sasl.enabled" .
+*/}}
+{{- define "fluss.security.sasl.enabled" -}}
+{{- $internal := include "fluss.security.listener.mechanism" (dict "context" 
.Values "listener" "internal") -}}
+{{- $client := include "fluss.security.listener.mechanism" (dict "context" 
.Values "listener" "client") -}}
+{{- if or (ne $internal "") (ne $client "") -}}true{{- end -}}
+{{- end -}}
+
+{{/*
+Returns true if any of the listeners uses 'plain' authentication mechanism.
+Usage:
+  include "fluss.security.sasl.plain.enabled" .
+*/}}
+{{- define "fluss.security.sasl.plain.enabled" -}}
+{{- $internal := include "fluss.security.listener.mechanism" (dict "context" 
.Values "listener" "internal") -}}
+{{- $client := include "fluss.security.listener.mechanism" (dict "context" 
.Values "listener" "client") -}}
+{{- if or (eq $internal "plain") (eq $client "plain") -}}true{{- end -}}
+{{- end -}}
+
+{{/*
+Returns protocol value derived from listener mechanism.
+Usage:
+  include "fluss.security.listener.protocol" (dict "context" .Values 
"listener" "internal")
+*/}}
+{{- define "fluss.security.listener.protocol" -}}
+{{- $mechanism := include "fluss.security.listener.mechanism" (dict "context" 
.context "listener" .listener) -}}
+{{- if eq $mechanism "" -}}PLAINTEXT{{- else -}}SASL{{- end -}}
+{{- end -}}
+
+{{/*
+Returns comma separated list of enabled mechanisms.
+Usage:
+  include "fluss.security.sasl.enabledMechanisms" .
+*/}}
+{{- define "fluss.security.sasl.enabledMechanisms" -}}
+{{- $mechanisms := list -}}
+{{- range $listener := list "internal" "client" -}}
+  {{- $current := include "fluss.security.listener.mechanism" (dict "context" 
$.Values "listener" $listener) -}}
+  {{- if and (ne $current "") (not (has (upper $current) $mechanisms)) -}}
+    {{- $mechanisms = append $mechanisms (upper $current) -}}
+  {{- end -}}
+{{- end -}}
+{{- join "," $mechanisms -}}
+{{- end -}}
+
+{{/*
+Validates that SASL mechanisms are valid.
+Returns an error message if invalid, empty string otherwise.
+Usage:
+  include "fluss.security.sasl.validateMechanisms" .
+*/}}
+{{- define "fluss.security.sasl.validateMechanisms" -}}
+{{- $allowedMechanisms := list "" "plain" -}}
+{{- range $listener := list "internal" "client" -}}
+  {{- $listenerValues := index $.Values.security $listener | default (dict) -}}
+  {{- $sasl := $listenerValues.sasl | default (dict) -}}
+  {{- $mechanism := lower (default "" $sasl.mechanism) -}}
+  {{- if not (has $mechanism $allowedMechanisms) -}}
+    {{- printf "security.%s.sasl.mechanism must be empty or: plain" $listener 
-}}
+  {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Validates that the client PLAIN mechanism block contains the required users.
+Returns an error message if invalid, empty string otherwise.
+Usage:
+  include "fluss.security.sasl.validateClientPlainUsers" .
+*/}}
+{{- define "fluss.security.sasl.validateClientPlainUsers" -}}
+{{- $clientMechanism := include "fluss.security.listener.mechanism" (dict 
"context" .Values "listener" "client") -}}
+{{- if eq $clientMechanism "plain" -}}
+  {{- $users := .Values.security.client.sasl.plain.users | default (list) -}}
+  {{- 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 -}}
+    {{- 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 -}}
+      {{- end -}}
+    {{- end -}}
+  {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Returns the default internal SASL username based on the release name.
+Usage:
+  include "fluss.security.sasl.plain.internal.defaultUsername" .
+*/}}
+{{- define "fluss.security.sasl.plain.internal.defaultUsername" -}}
+{{- printf "fluss-internal-user-%s" .Release.Name -}}
+{{- end -}}
+
+{{/*
+Returns the default internal SASL password based on the release name (sha256 
hashed).
+Usage:
+  include "fluss.security.sasl.plain.internal.defaultPassword" .
+*/}}
+{{- define "fluss.security.sasl.plain.internal.defaultPassword" -}}
+{{- printf "fluss-internal-password-%s" .Release.Name | sha256sum -}}
+{{- end -}}
+
+{{/*
+Returns the resolved internal SASL username (user-provided or auto-generated 
default).
+Usage:
+  include "fluss.security.sasl.plain.internal.username" .
+*/}}
+{{- define "fluss.security.sasl.plain.internal.username" -}}
+{{- .Values.security.internal.sasl.plain.username | default (include 
"fluss.security.sasl.plain.internal.defaultUsername" .) -}}
+{{- end -}}
+
+{{/*
+Returns the resolved internal SASL password (user-provided or auto-generated 
default).
+Usage:
+  include "fluss.security.sasl.plain.internal.password" .
+*/}}
+{{- define "fluss.security.sasl.plain.internal.password" -}}
+{{- .Values.security.internal.sasl.plain.password | default (include 
"fluss.security.sasl.plain.internal.defaultPassword" .) -}}
+{{- end -}}
+
+{{/*
+Returns a warning if the internal SASL user is using auto-generated 
credentials.
+Usage:
+  include "fluss.security.sasl.warnInternalUser" .
+*/}}
+{{- define "fluss.security.sasl.warnInternalUser" -}}
+{{- 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" -}}
+    {{- end -}}
+  {{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Compile all warnings and errors into a single message.
+Usage:
+  include "fluss.security.validateValues" .
+*/}}
+{{- define "fluss.security.validateValues" -}}
+
+{{- $errMessages := list -}}
+{{- $errMessages = append $errMessages (include 
"fluss.security.sasl.validateMechanisms" .) -}}
+{{- $errMessages = append $errMessages (include 
"fluss.security.sasl.validateClientPlainUsers" .) -}}
+
+{{- $errMessages = without $errMessages "" -}}
+{{- $errMessage := join "\n" $errMessages -}}
+
+{{- $warnMessages := list -}}
+{{- $warnMessages = append $warnMessages (include 
"fluss.security.sasl.warnInternalUser" .) -}}
+
+{{- $warnMessages = without $warnMessages "" -}}
+{{- $warnMessage := join "\n" $warnMessages -}}
+
+{{- if $warnMessage -}}
+{{-   printf "\nVALUES WARNING:\n%s" $warnMessage -}}
+{{- end -}}
+
+{{- if $errMessage -}}
+{{-   printf "\nVALUES VALIDATION:\n%s" $errMessage | fail -}}
+{{- end -}}
+
+{{- end -}}
+
+{{/*
+Returns the SASL JAAS config name.
+Usage:
+  include "fluss.security.sasl.configName" .
+*/}}
+{{- define "fluss.security.sasl.configName" -}}
+{{ include "fluss.fullname" . }}-sasl-jaas-config
+{{- end -}}
diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml
index b0e868c90..78c618c4d 100644
--- a/helm/templates/configmap.yaml
+++ b/helm/templates/configmap.yaml
@@ -26,4 +26,19 @@ data:
   server.yaml: |
     {{- range $key, $val := .Values.configurationOverrides }}
     {{ $key }}: {{ tpl (printf "%v" $val) $ }}
-    {{- end }}
\ No newline at end of file
+    {{- end }}
+
+    ### Security
+
+    {{- $internalProtocol := include "fluss.security.listener.protocol" (dict 
"context" .Values "listener" "internal") | trim -}}
+    {{- $clientProtocol := include "fluss.security.listener.protocol" (dict 
"context" .Values "listener" "client") | trim -}}
+    {{- $enabledMechanisms := include "fluss.security.sasl.enabledMechanisms" 
. | trim -}}
+    {{- $internalMechanism := include "fluss.security.listener.mechanism" 
(dict "context" .Values "listener" "internal") -}}
+    {{- if (include "fluss.security.sasl.enabled" .) }}
+    security.protocol.map: INTERNAL:{{ $internalProtocol }},CLIENT:{{ 
$clientProtocol }}
+    security.sasl.enabled.mechanisms: {{ $enabledMechanisms }}
+    {{- if ne $internalMechanism "" }}
+    client.security.protocol: SASL
+    client.security.sasl.mechanism: {{ upper $internalMechanism }}
+    {{- end }}
+    {{- end }}
diff --git a/helm/templates/secret-jaas-config.yaml 
b/helm/templates/secret-jaas-config.yaml
new file mode 100644
index 000000000..767ec8f05
--- /dev/null
+++ b/helm/templates/secret-jaas-config.yaml
@@ -0,0 +1,52 @@
+#
+# 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.
+#
+
+{{- if (include "fluss.security.sasl.plain.enabled" .) -}}
+{{- $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
+metadata:
+  name: {{ include "fluss.security.sasl.configName" . }}
+  labels:
+    {{- include "fluss.labels" . | nindent 4 }}
+type: Opaque
+stringData:
+  jaas.conf: |
+{{- if eq $internalMechanism "plain" }}
+    internal.FlussServer {
+       org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required
+       user_{{ $internalUsername }}="{{ $internalPassword }}";
+    };
+    FlussClient {
+       org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required
+       username="{{ $internalUsername }}"
+       password="{{ $internalPassword }}";
+    };
+{{- 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 }}"
+{{- end }};
+    };
+{{- end }}
+{{- end -}}
diff --git a/helm/templates/sts-coordinator.yaml 
b/helm/templates/sts-coordinator.yaml
index 6e187bd98..c443e230f 100644
--- a/helm/templates/sts-coordinator.yaml
+++ b/helm/templates/sts-coordinator.yaml
@@ -60,6 +60,10 @@ spec:
               valueFrom:
                 fieldRef:
                   fieldPath: status.hostIP
+            {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+            - name: FLUSS_ENV_JAVA_OPTS
+              value: 
"-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf"
+            {{- end }}
           ports:
             - name: internal
               containerPort: {{ .Values.listeners.internal.port }}
@@ -95,12 +99,17 @@ spec:
             tcpSocket:
               port: {{ .Values.listeners.client.port }}
           resources:
-            {{- toYaml .Values.resources.coordinatorServer | nindent 12 }}
+            {{- toYaml .Values.resources.tabletServer | nindent 12 }}
           volumeMounts:
             - name: fluss-conf
               mountPath: /opt/conf
             - name: data
               mountPath: /tmp/fluss/data
+            {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+            - name: sasl-config
+              mountPath: /etc/fluss/conf
+              readOnly: true
+            {{- end }}
       volumes:
         - name: fluss-conf
           configMap:
@@ -109,6 +118,11 @@ spec:
         - name: data
           emptyDir: {}
         {{- end }}
+        {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+        - name: sasl-config
+          secret:
+            secretName: {{ include "fluss.security.sasl.configName" . }}
+        {{- end }}
   {{- if .Values.coordinator.storage.enabled }}
   volumeClaimTemplates:
     - metadata:
diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml
index aa60cf462..1ffe1af31 100644
--- a/helm/templates/sts-tablet.yaml
+++ b/helm/templates/sts-tablet.yaml
@@ -56,6 +56,10 @@ spec:
               valueFrom:
                 fieldRef:
                   fieldPath: metadata.namespace
+            {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+            - name: FLUSS_ENV_JAVA_OPTS
+              value: 
"-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf"
+            {{- end }}
           ports:
             - name: internal
               containerPort: {{ .Values.listeners.internal.port }}
@@ -98,6 +102,11 @@ spec:
               mountPath: /opt/conf
             - name: data
               mountPath: /tmp/fluss/data
+            {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+            - name: sasl-config
+              mountPath: /etc/fluss/conf
+              readOnly: true
+            {{- end }}
       volumes:
         - name: fluss-conf
           configMap:
@@ -106,6 +115,11 @@ spec:
         - name: data
           emptyDir: {}
         {{- end }}
+        {{- if (include "fluss.security.sasl.plain.enabled" .) }}
+        - name: sasl-config
+          secret:
+            secretName: {{ include "fluss.security.sasl.configName" . }}
+        {{- end }}
   {{- if .Values.tablet.storage.enabled }}
   volumeClaimTemplates:
     - metadata:
diff --git a/helm/tests/security_test.yaml b/helm/tests/security_test.yaml
new file mode 100644
index 000000000..f6d67f508
--- /dev/null
+++ b/helm/tests/security_test.yaml
@@ -0,0 +1,284 @@
+#
+# 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: plain-sasl-config
+templates:
+  - templates/secret-jaas-config.yaml
+  - templates/configmap.yaml
+tests:
+  - it: renders JAAS secret and protocol map for plain mechanisms
+    template: templates/secret-jaas-config.yaml
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users:
+        - username: client-user
+          password: client-pass
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - hasDocuments:
+          count: 1
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'internal\.FlussServer\s*\{'
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'client\.FlussServer\s*\{'
+  - it: writes SASL protocol map and enabled mechanisms in configmap
+    template: templates/configmap.yaml
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users:
+        - username: client-user
+          password: client-pass
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.protocol\.map: INTERNAL:SASL,CLIENT:SASL'
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN'
+
+---
+
+suite: plain-validation
+templates:
+  - templates/NOTES.txt
+tests:
+  - it: fails when client plain users are empty
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users: []
+    asserts:
+      - failedTemplate:
+          errorMessage: "VALUES VALIDATION:\nsecurity.client.sasl.plain.users 
must contain at least one user when security.client.sasl.mechanism is plain"
+
+---
+
+suite: plain-internal-defaults
+templates:
+  - templates/secret-jaas-config.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"'
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'password="[a-f0-9]{64}"'
+
+---
+
+suite: security-disabled-defaults
+templates:
+  - templates/secret-jaas-config.yaml
+  - templates/sts-coordinator.yaml
+  - templates/configmap.yaml
+tests:
+  - it: does not render JAAS secret by default
+    template: templates/secret-jaas-config.yaml
+    asserts:
+      - hasDocuments:
+          count: 0
+  - it: does not render SASL settings in configmap by default
+    template: templates/configmap.yaml
+    asserts:
+      - notMatchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms:'
+      - notMatchRegex:
+          path: data["server.yaml"]
+          pattern: 'client\.security\.protocol: SASL'
+  - it: does not render SASL volumes by default
+    template: templates/sts-coordinator.yaml
+    asserts:
+      - notContains:
+          path: spec.template.spec.volumes
+          content:
+            name: sasl-config
+
+---
+
+suite: plain-internal-only
+templates:
+  - templates/secret-jaas-config.yaml
+  - templates/sts-coordinator.yaml
+  - templates/configmap.yaml
+tests:
+  - it: renders internal server and client JAAS blocks only
+    template: templates/secret-jaas-config.yaml
+    set:
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - hasDocuments:
+          count: 1
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'internal\.FlussServer\s*\{'
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'FlussClient\s*\{'
+      - notMatchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'client\.FlussServer\s*\{'
+  - it: renders security config in configmap for internal SASL
+    template: templates/configmap.yaml
+    set:
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.protocol\.map: INTERNAL:SASL,CLIENT:PLAINTEXT'
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN'
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'client\.security\.protocol: SASL'
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'client\.security\.sasl\.mechanism: PLAIN'
+  - it: renders JAAS env var and mounts secret volume
+    template: templates/sts-coordinator.yaml
+    set:
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[0].env
+          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-config
+            secret:
+              secretName: RELEASE-NAME-fluss-sasl-jaas-config
+
+---
+
+suite: plain-client-only
+templates:
+  - templates/secret-jaas-config.yaml
+  - templates/sts-coordinator.yaml
+  - templates/configmap.yaml
+tests:
+  - it: renders client JAAS block only
+    template: templates/secret-jaas-config.yaml
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users:
+        - username: app
+          password: app-pass
+    asserts:
+      - hasDocuments:
+          count: 1
+      - matchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'client\.FlussServer\s*\{'
+      - notMatchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'internal\.FlussServer\s*\{'
+      - notMatchRegex:
+          path: stringData["jaas.conf"]
+          pattern: 'FlussClient\s*\{'
+  - it: does not render internal client sasl config for coordinator when 
internal listener is plaintext
+    template: templates/configmap.yaml
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users:
+        - username: app
+          password: app-pass
+    asserts:
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.protocol\.map: INTERNAL:PLAINTEXT,CLIENT:SASL'
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN'
+      - notMatchRegex:
+          path: data["server.yaml"]
+          pattern: 'client\.security\.protocol: SASL'
+
+---
+
+suite: mechanism-validation
+templates:
+  - templates/NOTES.txt
+tests:
+  - it: fails for invalid client mechanism value
+    set:
+      security.client.sasl.mechanism: bogus
+    asserts:
+      - failedTemplate:
+          errorMessage: "VALUES VALIDATION:\nsecurity.client.sasl.mechanism 
must be empty or: plain"
+  - it: accepts empty mechanism as default
+    asserts:
+      - notFailedTemplate: {}
+
+---
+
+suite: mechanism-dedup-and-path
+templates:
+  - templates/configmap.yaml
+  - templates/sts-tablet.yaml
+tests:
+  - it: deduplicates enabled mechanisms when both listeners use plain
+    template: templates/configmap.yaml
+    set:
+      security.client.sasl.mechanism: plain
+      security.client.sasl.plain.users:
+        - username: app
+          password: app-pass
+      security.internal.sasl.mechanism: plain
+      security.internal.sasl.plain.username: internal-user
+      security.internal.sasl.plain.password: internal-pass
+    asserts:
+      - matchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN'
+      - notMatchRegex:
+          path: data["server.yaml"]
+          pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN,PLAIN'
+  - it: writes server yaml using expected destination path
+    template: templates/sts-tablet.yaml
+    asserts:
+      - matchRegex:
+          path: spec.template.spec.containers[0].command[2]
+          pattern: 'cp /opt/conf/server\.yaml \$FLUSS_HOME/conf'
+      - matchRegex:
+          path: spec.template.spec.containers[0].command[2]
+          pattern: '>> \$FLUSS_HOME/conf/server\.yaml'
diff --git a/helm/values.yaml b/helm/values.yaml
index db5d3fc2a..521553f67 100644
--- a/helm/values.yaml
+++ b/helm/values.yaml
@@ -58,6 +58,23 @@ listeners:
   client:
     port: 9124
 
+# Fluss security configurations
+security:
+  client:
+    sasl:
+      # "" | plain
+      mechanism: ""
+      plain:
+        users: []
+
+  internal:
+    sasl:
+      # "" | plain
+      mechanism: ""
+      plain:
+        username: ""
+        password: ""
+
 resources: {}
   # We usually recommend not to specify default resources and to leave this as 
a conscious
   # choice for the user. This also increases chances charts run on 
environments with little
diff --git a/website/docs/install-deploy/deploying-with-helm.md 
b/website/docs/install-deploy/deploying-with-helm.md
index 9bdbf414e..9bda9f1a8 100644
--- a/website/docs/install-deploy/deploying-with-helm.md
+++ b/website/docs/install-deploy/deploying-with-helm.md
@@ -36,7 +36,7 @@ the installation documentation provides instructions for 
deploying one using Bit
 
 ### Running Fluss locally with Minikube
 
-For local testing and development, you can deploy Fluss on Minikube. This is 
ideal for development, testing, and learning purposes.
+For local testing and development, you can deploy Fluss on Minikube. This is 
ideal for development, testing and learning purposes.
 
 #### Prerequisites
 
@@ -157,7 +157,7 @@ kubectl logs -l app.kubernetes.io/component=tablet
 
 ## Configuration Parameters
 
-The following table lists the configurable parameters of the Fluss chart and 
their default values.
+The following table lists the configurable parameters of the Fluss chart, and 
their default values.
 
 ### Global Parameters
 
@@ -183,6 +183,25 @@ The following table lists the configurable parameters of 
the Fluss chart and the
 | `listeners.internal.port` | Internal communication port | `9123` |
 | `listeners.client.port` | Client port (intra-cluster) | `9124` |
 
+### Security Configuration
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `security.client.sasl.mechanism` | Client listener SASL mechanism (`""`, 
`plain`) | `""` |
+| `security.internal.sasl.mechanism` | Internal listener SASL mechanism (`""`, 
`plain`) | `""` |
+| `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 | 
`""` |
+
+Only `plain` mechanism is supported for now. An empty string disables the SASL 
authentication, and maps to the `PLAINTEXT` protocol.
+
+If the internal SASL username or password is left empty, the chart 
automatically generates credentials based on the Helm release name:
+
+- Username is set to the `"fluss-internal-user-<release-name>"`
+- Password is set to the SHA-256 hash of 
`"fluss-internal-password-<release-name>"`
+
+It is recommended to set these explicitly in production.
+
 ### Fluss Configuration Overrides
 
 | Parameter | Description | Default |
@@ -253,6 +272,46 @@ listeners:
     port: 9123
   client:
     port: 9124
+
+security:
+  client:
+    sasl:
+      mechanism: ""
+  internal:
+    sasl:
+      mechanism: ""
+```
+
+### Enabling Secure Connection
+
+With the helm deployment, you can specify authentication mechanisms when 
connecting to the Fluss cluster.
+
+The following table shows the supported mechanisms and security they provide:
+
+| Mechanism | Method      | Authentication | TLS Encryption     |
+|:---------:|:-----------:|:--------------:|:------------------:|
+| `""`      | `PLAINTEXT` | No             | No                 |
+| `plain`   | `SASL`      | Yes            | No                 |
+
+By default, the `PLAINTEXT` protocol is used.
+
+You can set the SASL authentication by enabling `plain` mechanism.
+
+```yaml
+security:
+  client:
+    sasl:
+      mechanism: plain
+      plain:
+        users:
+          - username: client-user
+            password: client-password
+  internal:
+    sasl:
+      mechanism: plain
+      plain:
+        username: internal-user
+        password: internal-password
 ```
 
 ### Storage Configuration

Reply via email to