diff --git a/.github/workflows/helm_ci.yml b/.github/workflows/helm_ci.yml index 4ff5656bc..eecc5ce2a 100644 --- a/.github/workflows/helm_ci.yml +++ b/.github/workflows/helm_ci.yml @@ -68,6 +68,55 @@ jobs: grep -q "security-config" /tmp/security.yaml echo "Security configuration renders correctly" + echo "" + echo "=== Testing JWT expiration overrides ===" + helm template test $CHART_DIR \ + --set global.seaweedfs.securityConfig.jwtSigning.expiresAfterSeconds.volumeWrite=11 \ + > /tmp/jwt-volume-write-expiration.yaml + grep -q "security-config" /tmp/jwt-volume-write-expiration.yaml + grep -q "expires_after_seconds = 11" /tmp/jwt-volume-write-expiration.yaml + + helm template test $CHART_DIR \ + --set global.seaweedfs.securityConfig.jwtSigning.volumeRead=true \ + --set global.seaweedfs.securityConfig.jwtSigning.filerWrite=true \ + --set global.seaweedfs.securityConfig.jwtSigning.filerRead=true \ + --set global.seaweedfs.securityConfig.jwtSigning.expiresAfterSeconds.volumeWrite=11 \ + --set global.seaweedfs.securityConfig.jwtSigning.expiresAfterSeconds.volumeRead=22 \ + --set global.seaweedfs.securityConfig.jwtSigning.expiresAfterSeconds.filerWrite=33 \ + --set global.seaweedfs.securityConfig.jwtSigning.expiresAfterSeconds.filerRead=44 \ + > /tmp/jwt-expiration.yaml + + assert_jwt_expiration() { + local section="$1" + local seconds="$2" + awk -v section="[$section]" -v seconds="$seconds" ' + /^[[:space:]]*\[.*\][[:space:]]*$/ { + in_section = index($0, section) > 0 + } + in_section && $0 ~ "^[[:space:]]*expires_after_seconds = " seconds "$" { + found = 1 + } + END { exit !found } + ' /tmp/jwt-expiration.yaml + } + + assert_jwt_expiration jwt.signing 11 + assert_jwt_expiration jwt.signing.read 22 + assert_jwt_expiration jwt.filer_signing 33 + assert_jwt_expiration jwt.filer_signing.read 44 + + helm template test $CHART_DIR \ + --set global.seaweedfs.enableSecurity=true \ + --set global.seaweedfs.securityConfig.jwtSigning.volumeRead=true \ + --set global.seaweedfs.securityConfig.jwtSigning.filerWrite=true \ + --set global.seaweedfs.securityConfig.jwtSigning.filerRead=true \ + > /tmp/jwt-default-expiration.yaml + if grep -q "expires_after_seconds =" /tmp/jwt-default-expiration.yaml; then + echo "FAIL: zero JWT expiration values should preserve runtime defaults" + exit 1 + fi + echo "JWT expiration overrides render correctly" + echo "" echo "=== Testing IAM gRPC opt-in path ===" # Regression test: the filer registers the IAM gRPC service the diff --git a/k8s/charts/seaweedfs/templates/shared/_helpers.tpl b/k8s/charts/seaweedfs/templates/shared/_helpers.tpl index 1fa92a0c1..de5943a40 100644 --- a/k8s/charts/seaweedfs/templates/shared/_helpers.tpl +++ b/k8s/charts/seaweedfs/templates/shared/_helpers.tpl @@ -333,11 +333,13 @@ Create the name of the service account to use {{- end -}} {{/* True when security.toml should be rendered and mounted. volumeWrite is - excluded since it defaults to true. */}} + excluded unless its non-default expiration is configured. */}} {{- define "seaweedfs.securityConfigEnabled" -}} {{- $sec := (.Values.global.seaweedfs).securityConfig | default dict -}} {{- $jwt := $sec.jwtSigning | default dict -}} -{{- if or .Values.global.seaweedfs.enableSecurity $jwt.volumeRead $jwt.filerWrite $jwt.filerRead -}} +{{- $expiresAfterSeconds := $jwt.expiresAfterSeconds | default dict -}} +{{- $volumeWriteExpirationConfigured := and $jwt.volumeWrite (gt (int $expiresAfterSeconds.volumeWrite) 0) -}} +{{- if or .Values.global.seaweedfs.enableSecurity $volumeWriteExpirationConfigured $jwt.volumeRead $jwt.filerWrite $jwt.filerRead -}} true {{- end -}} {{- end -}} diff --git a/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml b/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml index 8b9486e13..a067e1de9 100644 --- a/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml +++ b/k8s/charts/seaweedfs/templates/shared/security-configmap.yaml @@ -19,40 +19,56 @@ data: {{- $existing = lookup "v1" "ConfigMap" .Release.Namespace $legacyName }} {{- end }} {{- $securityConfig := fromToml (dig "data" "security.toml" "" $existing) }} + {{- $securityConfigValues := .Values.global.seaweedfs.securityConfig | default dict }} + {{- $jwtSigning := $securityConfigValues.jwtSigning | default dict }} + {{- $expiresAfterSeconds := $jwtSigning.expiresAfterSeconds | default dict }} security.toml: |- # this file is read by master, volume server, and filer - {{- if .Values.global.seaweedfs.securityConfig.jwtSigning.volumeWrite }} + {{- if $jwtSigning.volumeWrite }} # the jwt signing key is read by master and volume server - # a jwt expires in 10 seconds + # the jwt defaults to expire after 10 seconds [jwt.signing] key = "{{ dig "jwt" "signing" "key" (randAlphaNum 10 | b64enc) $securityConfig }}" + {{- if gt (int $expiresAfterSeconds.volumeWrite) 0 }} + expires_after_seconds = {{ int $expiresAfterSeconds.volumeWrite }} + {{- end }} {{- end }} - {{- if .Values.global.seaweedfs.securityConfig.jwtSigning.volumeRead }} + {{- if $jwtSigning.volumeRead }} # this jwt signing key is read by master and volume server, and it is used for read operations: # - the Master server generates the JWT, which can be used to read a certain file on a volume server # - the Volume server validates the JWT on reading + # the jwt defaults to expire after 60 seconds [jwt.signing.read] key = "{{ dig "jwt" "signing" "read" "key" (randAlphaNum 10 | b64enc) $securityConfig }}" + {{- if gt (int $expiresAfterSeconds.volumeRead) 0 }} + expires_after_seconds = {{ int $expiresAfterSeconds.volumeRead }} + {{- end }} {{- end }} - {{- if .Values.global.seaweedfs.securityConfig.jwtSigning.filerWrite }} + {{- if $jwtSigning.filerWrite }} # If this JWT key is configured, Filer only accepts writes over HTTP if they are signed with this JWT: # - f.e. the S3 API Shim generates the JWT # - the Filer server validates the JWT on writing - # the jwt defaults to expire after 10 seconds. + # the jwt defaults to expire after 10 seconds [jwt.filer_signing] key = "{{ dig "jwt" "filer_signing" "key" (randAlphaNum 10 | b64enc) $securityConfig }}" + {{- if gt (int $expiresAfterSeconds.filerWrite) 0 }} + expires_after_seconds = {{ int $expiresAfterSeconds.filerWrite }} + {{- end }} {{- end }} - {{- if .Values.global.seaweedfs.securityConfig.jwtSigning.filerRead }} + {{- if $jwtSigning.filerRead }} # If this JWT key is configured, Filer only accepts reads over HTTP if they are signed with this JWT: # - f.e. the S3 API Shim generates the JWT # - the Filer server validates the JWT on reading - # the jwt defaults to expire after 10 seconds. + # the jwt defaults to expire after 60 seconds [jwt.filer_signing.read] key = "{{ dig "jwt" "filer_signing" "read" "key" (randAlphaNum 10 | b64enc) $securityConfig }}" + {{- if gt (int $expiresAfterSeconds.filerRead) 0 }} + expires_after_seconds = {{ int $expiresAfterSeconds.filerRead }} + {{- end }} {{- end }} {{- if .Values.global.seaweedfs.enableSecurity }} diff --git a/k8s/charts/seaweedfs/values.yaml b/k8s/charts/seaweedfs/values.yaml index 07b87fb0e..8e7f9f080 100644 --- a/k8s/charts/seaweedfs/values.yaml +++ b/k8s/charts/seaweedfs/values.yaml @@ -26,6 +26,13 @@ global: volumeRead: false filerWrite: false filerRead: false + # Positive values override SeaweedFS token lifetime defaults. + # Zero keeps the runtime defaults: 10s for writes and 60s for reads. + expiresAfterSeconds: + volumeWrite: 0 + volumeRead: 0 + filerWrite: 0 + filerRead: 0 # we will use this serviceAccountName for all ClusterRoles/ClusterRoleBindings serviceAccountName: "seaweedfs" serviceAccountAnnotations: {}