diff --git a/.github/workflows/helm-chart.yaml b/.github/workflows/helm-chart.yaml new file mode 100644 index 0000000000..7ae546ee9e --- /dev/null +++ b/.github/workflows/helm-chart.yaml @@ -0,0 +1,54 @@ +################################################################################ +# 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. +################################################################################ + +name: "Helm Chart" + +permissions: + contents: read + +on: + pull_request: + paths: + - 'helm/**' + - '.github/workflows/helm-chart.yaml' + push: + branches: [main, release-*, ci-*] + paths: + - 'helm/**' + - '.github/workflows/helm-chart.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} + cancel-in-progress: true + +jobs: + test-helm-chart: + name: "Helm Lint and Unittest" + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Run helm-unittest" + uses: d3adb5/helm-unittest-action@v2 + with: + helm-version: latest + charts: helm + + - name: "Lint Helm chart" + run: helm lint ./helm diff --git a/helm/README.md b/helm/README.md index f09750c489..00b109a846 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. @@ -80,7 +80,7 @@ This assumes, that Zookeeper is reachable at `zk-zookeeper..svc. ```bash helm install fluss ./fluss-helm \ - --set zookeeper.address= + --set configurationOverrides.zookeeper.address= ``` ## Configuration reference diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 79ae9d3106..53d0097ae8 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -63,4 +63,4 @@ Selector labels {{- define "fluss.selectorLabels" -}} app.kubernetes.io/name: {{ include "fluss.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/templates/_sasl.tpl b/helm/templates/_sasl.tpl new file mode 100644 index 0000000000..2c07b7d407 --- /dev/null +++ b/helm/templates/_sasl.tpl @@ -0,0 +1,175 @@ +# +# 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. +# + +{{/* +Return true if SASL is configured in any of the listener protocols +*/}} +{{- define "fluss.sasl.enabled" -}} +{{- $enabled := false -}} +{{- range $id, $l := .Values.listeners -}} + {{- if and (not $enabled) (regexFind "SASL" (upper $l.protocol)) -}} + {{- $enabled = true -}} + {{- end -}} +{{- end -}} +{{- if $enabled -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if ZooKeeper SASL is enabled +*/}} +{{- define "fluss.zookeeper.sasl.enabled" -}} +{{- $zkSaslUsername := default "" .Values.security.zookeeperSasl.username | trim -}} +{{- $zkSaslPassword := default "" .Values.security.zookeeperSasl.password | trim -}} +{{- if or (ne $zkSaslUsername "") (ne $zkSaslPassword "") -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if any JAAS configuration is required +*/}} +{{- define "fluss.jaas.required" -}} +{{- if or (include "fluss.sasl.enabled" .) (include "fluss.zookeeper.sasl.enabled" .) -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if listener protocol is SASL +Usage: include "fluss.listener.sasl.enabled" (dict "root" . "listener" "internal") +*/}} +{{- define "fluss.listener.sasl.enabled" -}} +{{- $listener := index .root.Values.listeners .listener -}} +{{- if and $listener $listener.protocol (regexFind "SASL" (upper $listener.protocol)) -}} +{{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Return upper-cased SASL mechanism for a listener (defaults to PLAIN) +Usage: include "fluss.listener.sasl.mechanism" (dict "root" . "listener" "internal") +*/}} +{{- define "fluss.listener.sasl.mechanism" -}} +{{- $listener := index .root.Values.listeners .listener -}} +{{- $security := default (dict) $listener.security -}} +{{- $mechanism := default "PLAIN" $security.mechanism -}} +{{- upper $mechanism -}} +{{- end -}} + +{{/* +Validate SASL mechanism and users for a listener. +Fails if mechanism is not PLAIN or if any user has empty username/password. +Usage: include "fluss.sasl.validateListener" (dict "listener" "internal" "mechanism" $mechanism "users" $users "saslEnabled" true) +*/}} +{{- define "fluss.sasl.validateListener" -}} +{{- if .saslEnabled -}} +{{- if ne .mechanism "PLAIN" -}} +{{- fail (printf "listeners.%s.security.mechanism must be PLAIN when listeners.%s.protocol is SASL, got %s" .listener .listener .mechanism) -}} +{{- end -}} +{{- range $idx, $user := .users -}} +{{- if or (empty $user.username) (empty $user.password) -}} +{{- fail (printf "listeners.%s.security.users[%d] must set both username and password" $.listener $idx) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Auto-generate internal SASL users when none are provided. +Returns a JSON-encoded list of user dicts. Preserves previously generated credentials from existing secret. +Usage: $internalUsers := include "fluss.sasl.autoGenerateInternalUsers" (dict "root" . "existingUsers" $users) | fromJsonArray +*/}} +{{- define "fluss.sasl.autoGenerateInternalUsers" -}} +{{- $users := .existingUsers -}} +{{- if gt (len $users) 0 -}} +{{- $users | toJson -}} +{{- else -}} +{{- $secretName := printf "%s-sasl-jaas-config" (include "fluss.fullname" .root) -}} +{{- $existingSecret := (lookup "v1" "Secret" .root.Release.Namespace $secretName) -}} +{{- $existingSecretData := default (dict) $existingSecret.data -}} +{{- $generatedUser := "" -}} +{{- $generatedPassword := "" -}} +{{- if hasKey $existingSecretData "internal-generated-username" -}} +{{- $generatedUser = (get $existingSecretData "internal-generated-username") | b64dec -}} +{{- end -}} +{{- if hasKey $existingSecretData "internal-generated-password" -}} +{{- $generatedPassword = (get $existingSecretData "internal-generated-password") | b64dec -}} +{{- end -}} +{{- if or (empty $generatedUser) (empty $generatedPassword) -}} +{{- $generatedUser = printf "internal-%s" ((randAlphaNum 12) | lower) -}} +{{- $generatedPassword = randAlphaNum 32 -}} +{{- end -}} +{{- list (dict "username" $generatedUser "password" $generatedPassword) | toJson -}} +{{- end -}} +{{- end -}} + +{{/* +Render a JAAS server block for a named context with the given users. +Usage: include "fluss.sasl.jaasServerBlock" (dict "name" "internal" "users" $users) +*/}} +{{- define "fluss.sasl.jaasServerBlock" }} + {{ .name }}.FlussServer { + org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required + {{- range .users }} + user_{{ .username }}="{{ .password }}" + {{- end }}; + }; +{{- end -}} + +{{/* +Render a JAAS client block for inter-node authentication. +Usage: include "fluss.sasl.jaasClientBlock" (dict "user" $user) +*/}} +{{- define "fluss.sasl.jaasClientBlock" }} + FlussClient { + org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required + username="{{ .user.username }}" + password="{{ .user.password }}"; + }; +{{- end -}} + +{{/* +Consolidate SASL configuration logic into a single context. +Returns a JSON object with: + internalSaslEnabled: bool + clientSaslEnabled: bool + internalUsers: list of {username, password} + clientUsers: list of {username, password} + internalUsersAutogenerated: bool +Usage: $saslConfig := include "fluss.sasl.config" . | fromJson +*/}} +{{- define "fluss.sasl.config" -}} +{{- $internalSaslEnabled := eq (include "fluss.listener.sasl.enabled" (dict "root" . "listener" "internal")) "true" -}} +{{- $clientSaslEnabled := eq (include "fluss.listener.sasl.enabled" (dict "root" . "listener" "client")) "true" -}} +{{- $internalSecurity := default (dict) .Values.listeners.internal.security -}} +{{- $clientSecurity := default (dict) .Values.listeners.client.security -}} +{{- $internalUsersProvided := default (list) $internalSecurity.users -}} +{{- $internalUsersAutogenerated := and $internalSaslEnabled (eq (len $internalUsersProvided) 0) -}} +{{- $internalUsers := include "fluss.sasl.autoGenerateInternalUsers" (dict "root" . "existingUsers" $internalUsersProvided) | fromJsonArray -}} +{{- $clientUsers := default (list) $clientSecurity.users -}} +{{- $internalMechanism := include "fluss.listener.sasl.mechanism" (dict "root" . "listener" "internal") -}} +{{- $clientMechanism := include "fluss.listener.sasl.mechanism" (dict "root" . "listener" "client") -}} +{{- include "fluss.sasl.validateListener" (dict "listener" "internal" "mechanism" $internalMechanism "users" $internalUsers "saslEnabled" $internalSaslEnabled) -}} +{{- if and $clientSaslEnabled (eq (len $clientUsers) 0) -}} +{{- fail "listeners.client.security.users must contain at least one user when listeners.client.protocol is SASL" -}} +{{- end -}} +{{- include "fluss.sasl.validateListener" (dict "listener" "client" "mechanism" $clientMechanism "users" $clientUsers "saslEnabled" $clientSaslEnabled) -}} +{{- dict "internalSaslEnabled" $internalSaslEnabled "clientSaslEnabled" $clientSaslEnabled "internalUsers" $internalUsers "clientUsers" $clientUsers "internalUsersAutogenerated" $internalUsersAutogenerated | toJson -}} +{{- end -}} diff --git a/helm/templates/secret-jaas-config.yaml b/helm/templates/secret-jaas-config.yaml new file mode 100644 index 0000000000..a290deabcc --- /dev/null +++ b/helm/templates/secret-jaas-config.yaml @@ -0,0 +1,72 @@ +# +# 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.jaas.required" .) }} +{{- $saslEnabled := include "fluss.sasl.enabled" . -}} +{{- $zookeeperSaslConfigured := include "fluss.zookeeper.sasl.enabled" . -}} +{{- $zkSaslUsername := default "" .Values.security.zookeeperSasl.username -}} +{{- $zkSaslPassword := default "" .Values.security.zookeeperSasl.password -}} +{{- $zkSaslLoginModuleClass := default "" .Values.security.zookeeperSasl.loginModuleClass -}} +{{- if and $zookeeperSaslConfigured (or (eq (trim $zkSaslUsername) "") (eq (trim $zkSaslPassword) "")) -}} +{{- fail "security.zookeeperSasl.username and security.zookeeperSasl.password must be set together" -}} +{{- end -}} +{{- if and $zookeeperSaslConfigured (eq (trim $zkSaslLoginModuleClass) "") -}} +{{- fail "security.zookeeperSasl.loginModuleClass must be set when ZooKeeper SASL is configured" -}} +{{- end -}} +{{/* --- Render Secret --- */}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "fluss.fullname" . }}-sasl-jaas-config + labels: + {{- include "fluss.labels" . | nindent 4 }} +type: Opaque +stringData: +{{- if $saslEnabled }} +{{- $saslConfig := include "fluss.sasl.config" . | fromJson -}} +{{- if $saslConfig.internalUsersAutogenerated }} + internal-generated-username: {{ (first $saslConfig.internalUsers).username | quote }} + internal-generated-password: {{ (first $saslConfig.internalUsers).password | quote }} +{{- end }} +{{- end }} + jaas.conf: | +{{- if $saslEnabled }} +{{- $saslConfig := include "fluss.sasl.config" . | fromJson -}} +{{- if $saslConfig.internalSaslEnabled }} +{{- include "fluss.sasl.jaasServerBlock" (dict "name" "internal" "users" $saslConfig.internalUsers) }} +{{- end }} +{{- if $saslConfig.clientSaslEnabled }} +{{- include "fluss.sasl.jaasServerBlock" (dict "name" "client" "users" $saslConfig.clientUsers) }} +{{- end }} +{{- if $saslConfig.internalSaslEnabled }} +{{- include "fluss.sasl.jaasClientBlock" (dict "user" (first $saslConfig.internalUsers)) }} +{{- end }} +{{- end }} +{{- if $zookeeperSaslConfigured }} + ZookeeperClient { + {{ $zkSaslLoginModuleClass }} required + username="{{ $zkSaslUsername }}" + password="{{ $zkSaslPassword }}"; + }; +{{- end }} +{{- if $zookeeperSaslConfigured }} + zookeeper-client.properties: | + zookeeper.sasl.client=true + zookeeper.sasl.clientconfig=ZookeeperClient +{{- end }} +{{- end -}} diff --git a/helm/templates/sts-coordinator.yaml b/helm/templates/sts-coordinator.yaml index 6f2ec40ffc..21b2e646f5 100644 --- a/helm/templates/sts-coordinator.yaml +++ b/helm/templates/sts-coordinator.yaml @@ -77,6 +77,21 @@ spec: echo "" >> $FLUSS_HOME/conf/server.yaml && \ echo "bind.listeners: ${BIND_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ echo "advertised.listeners: ${ADVERTISED_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.protocol.map: INTERNAL:{{ upper .Values.listeners.internal.protocol }},CLIENT:{{ upper .Values.listeners.client.protocol }}" >> $FLUSS_HOME/conf/server.yaml && \ + + {{- if (include "fluss.sasl.enabled" .) }} + echo "security.sasl.enabled.mechanisms: PLAIN" >> $FLUSS_HOME/conf/server.yaml && \ + {{ if eq (include "fluss.listener.sasl.enabled" (dict "root" . "listener" "internal")) "true" }} + echo "client.security.protocol: SASL" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.mechanism: {{ include "fluss.listener.sasl.mechanism" (dict "root" . "listener" "internal") }}" >> $FLUSS_HOME/conf/server.yaml && \ + {{ end }} + {{- end }} + {{- if (include "fluss.zookeeper.sasl.enabled" .) }} + echo "zookeeper.client.config-path: /etc/fluss/conf/zookeeper-client.properties" >> $FLUSS_HOME/conf/server.yaml && \ + {{- end }} + {{- if (include "fluss.jaas.required" .) }} + export FLUSS_ENV_JAVA_OPTS="-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf ${FLUSS_ENV_JAVA_OPTS}" && \ + {{- end }} bin/coordinator-server.sh start-foreground livenessProbe: @@ -94,12 +109,17 @@ spec: tcpSocket: port: {{ .Values.listeners.client.port }} resources: - {{- toYaml .Values.resources.tabletServer | nindent 12 }} + {{- toYaml .Values.resources.coordinatorServer | nindent 12 }} volumeMounts: - name: fluss-conf mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data + {{- if (include "fluss.jaas.required" .) }} + - name: sasl-config + mountPath: /etc/fluss/conf + readOnly: true + {{- end }} volumes: - name: fluss-conf configMap: @@ -108,6 +128,11 @@ spec: - name: data emptyDir: {} {{- end }} + {{- if (include "fluss.jaas.required" .) }} + - name: sasl-config + secret: + secretName: {{ include "fluss.fullname" . }}-sasl-jaas-config + {{- end }} {{- if .Values.coordinator.storage.enabled }} volumeClaimTemplates: - metadata: diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml index f329eb1f29..925bfb1424 100644 --- a/helm/templates/sts-tablet.yaml +++ b/helm/templates/sts-tablet.yaml @@ -74,6 +74,21 @@ spec: echo "tablet-server.id: ${FLUSS_SERVER_ID}" >> $FLUSS_HOME/conf/server.yaml && \ echo "bind.listeners: ${BIND_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ echo "advertised.listeners: ${ADVERTISED_LISTENERS}" >> $FLUSS_HOME/conf/server.yaml && \ + echo "security.protocol.map: INTERNAL:{{ upper .Values.listeners.internal.protocol }},CLIENT:{{ upper .Values.listeners.client.protocol }}" >> $FLUSS_HOME/conf/server.yaml && \ + + {{- if (include "fluss.sasl.enabled" .) }} + echo "security.sasl.enabled.mechanisms: PLAIN" >> $FLUSS_HOME/conf/server.yaml && \ + {{ if eq (include "fluss.listener.sasl.enabled" (dict "root" . "listener" "internal")) "true" }} + echo "client.security.protocol: SASL" >> $FLUSS_HOME/conf/server.yaml && \ + echo "client.security.sasl.mechanism: {{ include "fluss.listener.sasl.mechanism" (dict "root" . "listener" "internal") }}" >> $FLUSS_HOME/conf/server.yaml && \ + {{ end }} + {{- end }} + {{- if (include "fluss.zookeeper.sasl.enabled" .) }} + echo "zookeeper.client.config-path: /etc/fluss/conf/zookeeper-client.properties" >> $FLUSS_HOME/conf/server.yaml && \ + {{- end }} + {{- if (include "fluss.jaas.required" .) }} + export FLUSS_ENV_JAVA_OPTS="-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf ${FLUSS_ENV_JAVA_OPTS}" && \ + {{- end }} bin/tablet-server.sh start-foreground livenessProbe: @@ -97,6 +112,11 @@ spec: mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data + {{- if (include "fluss.jaas.required" .) }} + - name: sasl-config + mountPath: /etc/fluss/conf + readOnly: true + {{- end }} volumes: - name: fluss-conf configMap: @@ -105,6 +125,11 @@ spec: - name: data emptyDir: {} {{- end }} + {{- if (include "fluss.jaas.required" .) }} + - name: sasl-config + secret: + secretName: {{ include "fluss.fullname" . }}-sasl-jaas-config + {{- end }} {{- if .Values.tablet.storage.enabled }} volumeClaimTemplates: - metadata: diff --git a/helm/tests/sasl_test.yaml b/helm/tests/sasl_test.yaml new file mode 100644 index 0000000000..6bd3a60355 --- /dev/null +++ b/helm/tests/sasl_test.yaml @@ -0,0 +1,390 @@ +# +# 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-disabled-secret +templates: + - templates/secret-jaas-config.yaml +tests: + - it: does not render sasl secret when sasl is disabled + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + asserts: + - hasDocuments: + count: 0 +--- + +suite: sasl-disabled-statefulset +templates: + - templates/sts-coordinator.yaml +tests: + - it: does not render sasl settings in pod spec when sasl is disabled + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.enabled\.mechanisms:' + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.protocol: SASL' + - notContains: + path: spec.template.spec.volumes + content: + name: sasl-config +--- + +suite: internal-sasl-generated-users +templates: + - templates/secret-jaas-config.yaml +tests: + - it: generates internal user and fluss client section when internal users are absent + set: + listeners.internal.protocol: SASL + listeners.client.protocol: PLAINTEXT + listeners.internal.security.users: [] + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'user_internal-[a-z0-9]{12}="[A-Za-z0-9]{32}"' + - matchRegex: + path: stringData["internal-generated-username"] + pattern: '^internal-[a-z0-9]{12}$' + - matchRegex: + path: stringData["internal-generated-password"] + pattern: '^[A-Za-z0-9]{32}$' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'FlussClient\s*\{' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'client\.FlussServer\s*\{' +--- + +suite: client-sasl-empty-users-fails +templates: + - templates/secret-jaas-config.yaml +tests: + - it: fails when client sasl users are empty + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: SASL + listeners.client.security.users: [] + asserts: + - failedTemplate: + errorMessage: listeners.client.security.users must contain at least one user when listeners.client.protocol is SASL +--- + +suite: client-sasl-secret +templates: + - templates/secret-jaas-config.yaml +tests: + - it: renders client listener users and skips fluss client section when internal listener is plaintext + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: SASL + listeners.client.security.users: + - username: app + password: app-password + - username: reader + password: reader-password + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'client\.FlussServer\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'user_app="app-password"' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'user_reader="reader-password"' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'FlussClient\s*\{' +--- + +suite: sasl-missing-security-block +templates: + - templates/secret-jaas-config.yaml +tests: + - it: renders with generated internal user when internal security block is null + set: + listeners.internal.protocol: SASL + listeners.client.protocol: PLAINTEXT + listeners.internal.security: null + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - matchRegex: + path: stringData["internal-generated-username"] + pattern: '^internal-[a-z0-9]{12}$' +--- + +suite: both-listeners-sasl +templates: + - templates/secret-jaas-config.yaml + - templates/sts-coordinator.yaml + - templates/sts-tablet.yaml +tests: + - it: renders separate jaas entries for internal and client listeners + template: templates/secret-jaas-config.yaml + set: + listeners.internal.protocol: SASL + listeners.client.protocol: SASL + listeners.internal.security.users: + - username: internal-user + password: internal-password + listeners.client.security.users: + - username: external-user + password: external-password + asserts: + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'user_internal-user="internal-password"' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'client\.FlussServer\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'user_external-user="external-password"' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'FlussClient\s*\{' + - it: wires jaas and internal sasl client settings into coordinator + template: templates/sts-coordinator.yaml + set: + listeners.internal.protocol: SASL + listeners.client.protocol: SASL + listeners.internal.security.users: + - username: internal-user + password: internal-password + listeners.client.security.users: + - username: external-user + password: external-password + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: sasl-config + secret: + secretName: RELEASE-NAME-fluss-sasl-jaas-config + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.protocol: SASL' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.sasl\.mechanism: PLAIN' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'FLUSS_ENV_JAVA_OPTS="-Djava\.security\.auth\.login\.config=/etc/fluss/conf/jaas\.conf \$\{FLUSS_ENV_JAVA_OPTS\}"' + - it: wires jaas and internal sasl client settings into tablet + template: templates/sts-tablet.yaml + set: + listeners.internal.protocol: SASL + listeners.client.protocol: SASL + listeners.internal.security.users: + - username: internal-user + password: internal-password + listeners.client.security.users: + - username: external-user + password: external-password + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: sasl-config + secret: + secretName: RELEASE-NAME-fluss-sasl-jaas-config + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.protocol: SASL' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'client\.security\.sasl\.mechanism: PLAIN' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'FLUSS_ENV_JAVA_OPTS="-Djava\.security\.auth\.login\.config=/etc/fluss/conf/jaas\.conf \$\{FLUSS_ENV_JAVA_OPTS\}"' +--- + +suite: non-plain-mechanism-fails +templates: + - templates/secret-jaas-config.yaml +tests: + - it: fails when internal mechanism is not plain + set: + listeners.internal.protocol: SASL + listeners.client.protocol: PLAINTEXT + listeners.internal.security.mechanism: SCRAM + listeners.internal.security.users: + - username: internal-user + password: internal-password + asserts: + - failedTemplate: + errorMessage: listeners.internal.security.mechanism must be PLAIN when listeners.internal.protocol is SASL, got SCRAM + - it: fails when client mechanism is not plain + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: SASL + listeners.client.security.mechanism: SCRAM + listeners.client.security.users: + - username: external-user + password: external-password + asserts: + - failedTemplate: + errorMessage: listeners.client.security.mechanism must be PLAIN when listeners.client.protocol is SASL, got SCRAM +--- + +suite: server-yaml-write-path +templates: + - templates/sts-tablet.yaml +tests: + - it: writes server yaml using expected destination path + 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' +--- + +suite: zookeeper-sasl-enabled-secret +templates: + - templates/secret-jaas-config.yaml +tests: + - it: renders zookeeper client JAAS block when zookeeper sasl is configured + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + security.zookeeperSasl.username: zk-username + security.zookeeperSasl.password: zk-password + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'ZookeeperClient\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'org\.apache\.fluss\.shaded\.zookeeper3\.org\.apache\.zookeeper\.server\.auth\.DigestLoginModule required' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'client\.FlussServer\s*\{' + - equal: + path: stringData["zookeeper-client.properties"] + value: | + zookeeper.sasl.client=true + zookeeper.sasl.clientconfig=ZookeeperClient +--- + +suite: zookeeper-sasl-enabled-statefulset +templates: + - templates/sts-coordinator.yaml +tests: + - it: wires zookeeper client config path and JAAS mount for zookeeper sasl + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + security.zookeeperSasl.username: zk-username + security.zookeeperSasl.password: zk-password + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: sasl-config + secret: + secretName: RELEASE-NAME-fluss-sasl-jaas-config + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'zookeeper\.client\.config-path: /etc/fluss/conf/zookeeper-client\.properties' + - matchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'FLUSS_ENV_JAVA_OPTS="-Djava\.security\.auth\.login\.config=/etc/fluss/conf/jaas\.conf \$\{FLUSS_ENV_JAVA_OPTS\}"' + - notMatchRegex: + path: spec.template.spec.containers[0].command[2] + pattern: 'security\.sasl\.enabled\.mechanisms: PLAIN' +--- + +suite: zookeeper-sasl-missing-credentials-fails +templates: + - templates/secret-jaas-config.yaml +tests: + - it: fails when zookeeper sasl password is missing + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + security.zookeeperSasl.username: zk-username + asserts: + - failedTemplate: + errorMessage: security.zookeeperSasl.username and security.zookeeperSasl.password must be set together +--- + +suite: zookeeper-sasl-custom-login-module +templates: + - templates/secret-jaas-config.yaml +tests: + - it: renders custom zookeeper sasl login module when provided + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + security.zookeeperSasl.username: zk-username + security.zookeeperSasl.password: zk-password + security.zookeeperSasl.loginModuleClass: org.apache.zookeeper.server.auth.DigestLoginModule + asserts: + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'org\.apache\.zookeeper\.server\.auth\.DigestLoginModule required' +--- + +suite: zookeeper-sasl-empty-login-module-fails +templates: + - templates/secret-jaas-config.yaml +tests: + - it: fails when zookeeper sasl login module class is empty + set: + listeners.internal.protocol: PLAINTEXT + listeners.client.protocol: PLAINTEXT + security.zookeeperSasl.username: zk-username + security.zookeeperSasl.password: zk-password + security.zookeeperSasl.loginModuleClass: "" + asserts: + - failedTemplate: + errorMessage: security.zookeeperSasl.loginModuleClass must be set when ZooKeeper SASL is configured diff --git a/helm/values.yaml b/helm/values.yaml index db5d3fc2a9..63b1277454 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -54,9 +54,23 @@ coordinator: # Fluss listener configurations listeners: internal: + protocol: PLAINTEXT port: 9123 + security: + mechanism: PLAIN + users: [] client: + protocol: PLAINTEXT port: 9124 + security: + mechanism: PLAIN + users: [] + +security: + zookeeperSasl: + username: "" + password: "" + loginModuleClass: "org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule" resources: {} # We usually recommend not to specify default resources and to leave this as a conscious diff --git a/website/docs/install-deploy/deploying-with-helm.md b/website/docs/install-deploy/deploying-with-helm.md index 9bdbf414e5..323582b370 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 @@ -225,6 +225,17 @@ The following table lists the configurable parameters of the Fluss chart and the | `resources.tabletServer.limits.cpu` | CPU limits for tablet servers | Not set | | `resources.tabletServer.limits.memory` | Memory limits for tablet servers | Not set | +### SASL Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `listeners.internal.security.mechanism` | SASL mechanism for internal listener | `PLAIN` | +| `listeners.internal.security.users` | User list for internal SASL authentication (auto-generated if empty and internal protocol is SASL) | `[]` | +| `listeners.client.security.mechanism` | SASL mechanism for client listener | `PLAIN` | +| `listeners.client.security.users` | User list for client SASL authentication (required when client protocol is SASL) | `[]` | +| `security.zookeeperSasl.username` | ZooKeeper SASL username for Digest authentication | `""` | +| `security.zookeeperSasl.password` | ZooKeeper SASL password for Digest authentication | `""` | +| `security.zookeeperSasl.loginModuleClass` | JAAS login module class for ZooKeeper client SASL | `org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule` | ## Advanced Configuration @@ -245,16 +256,85 @@ The chart automatically configures listeners for internal cluster communication - **Internal Port (9123)**: Used for internal communication within the cluster - **Client Port (9124)**: Used for client connections -Custom listener configuration: +Default listeners configuration: + +```yaml +listeners: + internal: + protocol: PLAINTEXT + port: 9123 + security: + mechanism: PLAIN + users: [] + client: + protocol: PLAINTEXT + port: 9124 + security: + mechanism: PLAIN + users: [] +``` + +To enable SASL based authentication, set any of the protocols to `SASL`. + +### Enabling Secure Connection + +With the helm deployment, you can specify authentication protocols when connecting to the Fluss cluster. + +The following table shows the supported protocols and security they provide: + +| Method | Authentication | TLS Encryption | +|-------------|:--------------:|:------------------:| +| `PLAINTEXT` | No | No | +| `SASL` | Yes | No | + +By default, the `PLAINTEXT` protocol is used. + +The SASL authentication will be enabled if any of the listener protocols is using `SASL`. + +Set these values for additional configurations: ```yaml listeners: internal: + protocol: SASL port: 9123 + security: + mechanism: PLAIN + users: + - username: internal-admin + password: internal-password client: + protocol: SASL port: 9124 + security: + mechanism: PLAIN + users: + - username: client-admin + password: client-password +``` + +The `listeners.internal.security.users` and `listeners.client.security.users` fields define distinct user lists for internal and client SASL/PLAIN authentication. + +- When `listeners.internal.protocol` is `SASL` and no internal users are provided, the chart auto-generates one internal user and stores it in the Secret for reuse on upgrades. +- When `listeners.client.protocol` is `SASL`, `listeners.client.security.users` must include at least one `{username, password}` entry or rendering fails. +- The default authentication mechanism is `PLAIN` for both listeners, and non-`PLAIN` values fail template rendering. + +### Enabling ZooKeeper SASL Authentication + +If your ZooKeeper requires SASL authentication, configure the Fluss chart values as following: + +```yaml +security: + zookeeperSasl: + username: zk-username + password: zk-password + loginModuleClass: org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule ``` +When both `username` and `password` are set, the chart generates ZooKeeper client JAAS config and sets `zookeeper.client.config-path` automatically. Both username and password fields must be set together. + +If your runtime expects a different Zookeeper login module class name (for example shaded ZooKeeper), override `security.zookeeperSasl.loginModuleClass` field. + ### Storage Configuration Configure different storage volumes for coordinator or tablet pods: