diff --git a/helm/templates/_security.tpl b/helm/templates/_security.tpl index 210a35746f..f3684fcd47 100644 --- a/helm/templates/_security.tpl +++ b/helm/templates/_security.tpl @@ -29,6 +29,28 @@ Usage: {{- $mechanism -}} {{- end -}} +{{/* +Returns the ZooKeeper SASL authentication mechanism value. +Allowed mechanism values: '', 'plain' +Usage: + include "fluss.security.zookeeper.sasl.mechanism" . +*/}} +{{- define "fluss.security.zookeeper.sasl.mechanism" -}} +{{- $sasl := .Values.security.zookeeper.sasl | default (dict) -}} +{{- $mechanism := lower (default "" $sasl.mechanism) -}} +{{- $mechanism -}} +{{- end -}} + +{{/* +Returns true if ZooKeeper SASL authentication is enabled (mechanism is non-empty). +Usage: + include "fluss.security.zookeeper.sasl.enabled" . +*/}} +{{- define "fluss.security.zookeeper.sasl.enabled" -}} +{{- $mechanism := include "fluss.security.zookeeper.sasl.mechanism" . -}} +{{- if ne $mechanism "" -}}true{{- end -}} +{{- end -}} + {{/* Returns true if any of the listeners uses SASL based authentication mechanism ('plain' for now). Usage: @@ -117,6 +139,56 @@ Usage: {{- end -}} {{- end -}} +{{/* +Validates that ZooKeeper SASL mechanism is valid. +Returns an error message if invalid, empty string otherwise. +Usage: + include "fluss.security.zookeeper.sasl.validateMechanism" . +*/}} +{{- define "fluss.security.zookeeper.sasl.validateMechanism" -}} +{{- $allowedMechanisms := list "" "plain" -}} +{{- $mechanism := include "fluss.security.zookeeper.sasl.mechanism" . -}} +{{- if not (has $mechanism $allowedMechanisms) -}} + {{- print "security.zookeeper.sasl.mechanism must be empty or: plain" -}} +{{- end -}} +{{- end -}} + +{{/* +Validates that ZooKeeper SASL loginModuleClass is not empty when ZK SASL is enabled. +Returns an error message if invalid, empty string otherwise. +Usage: + include "fluss.security.zookeeper.sasl.validateLoginModuleClass" . +*/}} +{{- define "fluss.security.zookeeper.sasl.validateLoginModuleClass" -}} +{{- if and (include "fluss.security.zookeeper.sasl.enabled" .) (not .Values.security.zookeeper.sasl.plain.loginModuleClass) -}} + {{- print "security.zookeeper.sasl.plain.loginModuleClass must not be empty when security.zookeeper.sasl.mechanism is plain" -}} +{{- end -}} +{{- end -}} + +{{/* +Validates that ZooKeeper SASL username is not empty when ZK SASL is enabled. +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" -}} +{{- end -}} +{{- end -}} + +{{/* +Validates that ZooKeeper SASL password is not empty when ZK SASL is enabled. +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" -}} +{{- end -}} +{{- end -}} + {{/* Returns the default internal SASL username based on the release name. Usage: @@ -153,6 +225,17 @@ Usage: {{- .Values.security.internal.sasl.plain.password | default (include "fluss.security.sasl.plain.internal.defaultPassword" .) -}} {{- end -}} +{{/* +Returns true if JAAS configuration is required, either by listeners using SASL protocol or ZooKeeper SASL enablement. +Usage: + include "fluss.security.jaas.required" . +*/}} +{{- define "fluss.security.jaas.required" -}} +{{- if or (include "fluss.security.sasl.enabled" .) (include "fluss.security.zookeeper.sasl.enabled" .) -}} +{{- true -}} +{{- end -}} +{{- end -}} + {{/* Returns a warning if the internal SASL user is using auto-generated credentials. Usage: @@ -179,6 +262,10 @@ Usage: {{- $errMessages := list -}} {{- $errMessages = append $errMessages (include "fluss.security.sasl.validateMechanisms" .) -}} {{- $errMessages = append $errMessages (include "fluss.security.sasl.validateClientPlainUsers" .) -}} +{{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validateMechanism" .) -}} +{{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validateLoginModuleClass" .) -}} +{{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validateUsername" .) -}} +{{- $errMessages = append $errMessages (include "fluss.security.zookeeper.sasl.validatePassword" .) -}} {{- $errMessages = without $errMessages "" -}} {{- $errMessage := join "\n" $errMessages -}} @@ -202,8 +289,8 @@ Usage: {{/* Returns the SASL JAAS config name. Usage: - include "fluss.security.sasl.configName" . + include "fluss.security.jaas.configName" . */}} -{{- define "fluss.security.sasl.configName" -}} +{{- define "fluss.security.jaas.configName" -}} {{ include "fluss.fullname" . }}-sasl-jaas-config {{- end -}} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 66f3eb095d..c4574b6c17 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -53,3 +53,9 @@ data: {{- end }} {{- end }} + + ### Zookeeper + + {{- if (include "fluss.security.zookeeper.sasl.enabled" .) }} + zookeeper.client.config.path: /etc/fluss/conf/zookeeper-client.properties + {{- end }} diff --git a/helm/templates/secret-jaas-config.yaml b/helm/templates/secret-jaas-config.yaml index 6ed411acbe..436a2fbb28 100644 --- a/helm/templates/secret-jaas-config.yaml +++ b/helm/templates/secret-jaas-config.yaml @@ -16,7 +16,7 @@ # limitations under the License. # -{{ if (include "fluss.security.sasl.plain.enabled" .) }} +{{ 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" . -}} @@ -24,12 +24,13 @@ apiVersion: v1 kind: Secret metadata: - name: {{ include "fluss.security.sasl.configName" . }} + name: {{ include "fluss.security.jaas.configName" . }} labels: {{- include "fluss.labels" . | nindent 4 }} type: Opaque stringData: jaas.conf: | +{{- if (include "fluss.security.sasl.plain.enabled" .) }} {{- if eq $internalMechanism "plain" }} internal.FlussServer { org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required @@ -49,4 +50,16 @@ stringData: {{- end }}; }; {{- end }} +{{- end }} +{{- 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 }}"; + }; +{{- end }} +{{- if (include "fluss.security.zookeeper.sasl.enabled" .) }} + zookeeper-client.properties: | + zookeeper.sasl.clientconfig=ZookeeperClient +{{- end }} {{- end -}} diff --git a/helm/templates/sts-coordinator.yaml b/helm/templates/sts-coordinator.yaml index c443e230fb..7a6774b281 100644 --- a/helm/templates/sts-coordinator.yaml +++ b/helm/templates/sts-coordinator.yaml @@ -60,7 +60,7 @@ spec: valueFrom: fieldRef: fieldPath: status.hostIP - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: FLUSS_ENV_JAVA_OPTS value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" {{- end }} @@ -105,7 +105,7 @@ spec: mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: sasl-config mountPath: /etc/fluss/conf readOnly: true @@ -118,10 +118,10 @@ spec: - name: data emptyDir: {} {{- end }} - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: sasl-config secret: - secretName: {{ include "fluss.security.sasl.configName" . }} + secretName: {{ include "fluss.security.jaas.configName" . }} {{- end }} {{- if .Values.coordinator.storage.enabled }} volumeClaimTemplates: diff --git a/helm/templates/sts-tablet.yaml b/helm/templates/sts-tablet.yaml index 1ffe1af310..b58fc49f8b 100644 --- a/helm/templates/sts-tablet.yaml +++ b/helm/templates/sts-tablet.yaml @@ -56,7 +56,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: FLUSS_ENV_JAVA_OPTS value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" {{- end }} @@ -102,7 +102,7 @@ spec: mountPath: /opt/conf - name: data mountPath: /tmp/fluss/data - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: sasl-config mountPath: /etc/fluss/conf readOnly: true @@ -115,10 +115,10 @@ spec: - name: data emptyDir: {} {{- end }} - {{- if (include "fluss.security.sasl.plain.enabled" .) }} + {{- if (include "fluss.security.jaas.required" .) }} - name: sasl-config secret: - secretName: {{ include "fluss.security.sasl.configName" . }} + secretName: {{ include "fluss.security.jaas.configName" . }} {{- end }} {{- if .Values.tablet.storage.enabled }} volumeClaimTemplates: diff --git a/helm/tests/security_test.yaml b/helm/tests/security_test.yaml index f6d67f508e..d73e2604ea 100644 --- a/helm/tests/security_test.yaml +++ b/helm/tests/security_test.yaml @@ -282,3 +282,234 @@ tests: - 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 ZookeeperClient JAAS block when ZK SASL is enabled + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + asserts: + - hasDocuments: + count: 1 + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'ZookeeperClient\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'username="zk-user"' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'password="zk-pass"' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'DigestLoginModule required' + - matchRegex: + path: stringData["zookeeper-client.properties"] + pattern: 'zookeeper\.sasl\.clientconfig=ZookeeperClient' + - it: renders both SASL and ZK JAAS blocks when both are enabled + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: internal-user + security.internal.sasl.plain.password: internal-pass + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + asserts: + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'ZookeeperClient\s*\{' + - it: does not render SASL listener blocks when only ZK SASL is enabled + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + asserts: + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'internal\.FlussServer\s*\{' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'FlussClient\s*\{' + +--- + +suite: zookeeper-sasl-enabled-statefulset +templates: + - templates/sts-coordinator.yaml + - templates/sts-tablet.yaml + - templates/configmap.yaml +tests: + - it: renders JAAS env var and mounts secret volume for coordinator when ZK SASL is enabled + template: templates/sts-coordinator.yaml + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-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 + - it: renders JAAS env var and mounts secret volume for tablet when ZK SASL is enabled + template: templates/sts-tablet.yaml + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-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 + - it: renders zookeeper client config path in configmap when ZK SASL is enabled + template: templates/configmap.yaml + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + asserts: + - matchRegex: + path: data["server.yaml"] + pattern: 'zookeeper\.client\.config\.path: /etc/fluss/conf/zookeeper-client\.properties' + +--- + +suite: zookeeper-sasl-custom-login-module +templates: + - templates/secret-jaas-config.yaml +tests: + - it: renders custom login module class in ZookeeperClient JAAS block + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + security.zookeeper.sasl.plain.loginModuleClass: "com.example.CustomLoginModule" + asserts: + - matchRegex: + path: stringData["jaas.conf"] + pattern: 'com\.example\.CustomLoginModule required' + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'DigestLoginModule' + +--- + +suite: zookeeper-sasl-disabled +templates: + - templates/secret-jaas-config.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 + set: + security.internal.sasl.mechanism: plain + security.internal.sasl.plain.username: internal-user + security.internal.sasl.plain.password: internal-pass + asserts: + - hasDocuments: + count: 1 + - notMatchRegex: + path: stringData["jaas.conf"] + pattern: 'ZookeeperClient\s*\{' + - isNull: + path: stringData["zookeeper-client.properties"] + - it: does not render JAAS secret when neither SASL nor ZK SASL is enabled + template: templates/secret-jaas-config.yaml + asserts: + - hasDocuments: + count: 0 + - it: does not render JAAS env var or volume mount for coordinator when ZK SASL is disabled + template: templates/sts-coordinator.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: FLUSS_ENV_JAVA_OPTS + value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" + - notContains: + path: spec.template.spec.volumes + content: + name: sasl-config + any: true + - it: does not render JAAS env var or volume mount for tablet when ZK SASL is disabled + template: templates/sts-tablet.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: FLUSS_ENV_JAVA_OPTS + value: "-Djava.security.auth.login.config=/etc/fluss/conf/jaas.conf" + - notContains: + path: spec.template.spec.volumes + content: + name: sasl-config + any: true + - it: does not render zookeeper client config path in configmap when ZK SASL is disabled + template: templates/configmap.yaml + asserts: + - notMatchRegex: + path: data["server.yaml"] + pattern: 'zookeeper\.client\.config\.path' + +--- + +suite: zookeeper-sasl-validation +templates: + - templates/NOTES.txt +tests: + - it: fails when loginModuleClass is empty and ZK SASL is enabled + set: + security.zookeeper.sasl.mechanism: plain + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + security.zookeeper.sasl.plain.loginModuleClass: "" + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.plain.loginModuleClass must not be empty when security.zookeeper.sasl.mechanism is plain" + - it: fails when ZK SASL is enabled but username is empty + set: + security.zookeeper.sasl.mechanism: plain + 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" + - 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" + - it: fails for invalid zookeeper mechanism value + set: + security.zookeeper.sasl.mechanism: bogus + security.zookeeper.sasl.plain.username: zk-user + security.zookeeper.sasl.plain.password: zk-pass + asserts: + - failedTemplate: + errorMessage: "VALUES VALIDATION:\nsecurity.zookeeper.sasl.mechanism must be empty or: plain" diff --git a/helm/values.yaml b/helm/values.yaml index 63c11e7241..219fbc75ce 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -75,6 +75,15 @@ security: username: "" password: "" + zookeeper: + sasl: + # "" | plain + mechanism: "" + plain: + username: "" + password: "" + loginModuleClass: "org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.server.auth.DigestLoginModule" + metrics: reporters: "" prometheus: diff --git a/website/docs/install-deploy/deploying-with-helm.md b/website/docs/install-deploy/deploying-with-helm.md index ba9a8932bc..f7e543934d 100644 --- a/website/docs/install-deploy/deploying-with-helm.md +++ b/website/docs/install-deploy/deploying-with-helm.md @@ -202,6 +202,15 @@ If the internal SASL username or password is left empty, the chart automatically It is recommended to set these explicitly in production. +#### ZooKeeper SASL Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `security.zookeeper.sasl.mechanism` | ZooKeeper SASL mechanism (`""`, `plain`) | `""` | +| `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` | + ### Metrics Parameters | Parameter | Description | Default | @@ -325,6 +334,24 @@ security: password: internal-password ``` +### Enabling ZooKeeper SASL Authentication + +You can enable ZooKeeper ensemble SASL authentication, with the following values in the Fluss Helm chart: + +```yaml +security: + zookeeper: + sasl: + mechanism: plain + plain: + username: fluss-zk-user + password: fluss-zk-password +``` + +The `security.zookeeper.sasl.plain.username` and `security.zookeeper.sasl.plain.password` fields are required when `security.zookeeper.sasl.mechanism` is set to `plain`. + +ZooKeeper SASL can be enabled independently or together with the listeners SASL authentication. + ### Metrics and Monitoring When `metrics.reporters` is set, the chart adds the following `server.yaml` configuration entries: