helm: generate the SFTP host key per install (#10390)

* helm: generate the SFTP host key per install

The SFTP secret template shipped one fixed ed25519 host key, so every
install that did not override it presented the same host identity.
Generate the key at install time instead, following the
getOrGeneratePassword pattern: an existing secret keeps its key across
upgrades, except the previously bundled one, which is replaced with a
freshly generated key on the next upgrade.

* helm: create the SFTP host-keys secret the deployments mount

Both the sftp and all-in-one deployments mount /etc/sw/ssh from
<fullname>-sftp-ssh-secret, but no template created it, so a default
install could not start its pod and host keys only reached the server
when enableAuth happened to mount them elsewhere. Create the secret
with a generated ed25519 key, keeping whatever keys an existing secret
already holds. The sshPrivateKey default becomes empty: the file it
pointed at only exists when enableAuth mounts /etc/sw, and a configured
but missing key file is fatal to the server, while hostKeysFolder now
always has a key.

* helm: test SFTP host key generation and secret lifecycle

Template checks: keys render into the secret the deployments mount,
parse as PKCS#8 ed25519, differ between installs, and the render
carries no key material from the chart itself; existingSshConfigSecret
and all-in-one wiring covered. On the kind cluster, exercise the
secret lifecycle: a generated key survives upgrades, the key earlier
chart versions bundled is replaced, and operator-managed keys are kept
untouched. chart-testing now also installs with sftp enabled, where
the pod only becomes ready if the server loads the generated host
key.

* helm: treat a whitespace-only stored SFTP host key as missing

A whitespace-only secret value skipped regeneration and then rendered
an empty key file.

* helm: mount the SFTP host keys secret at the configured hostKeysFolder

The secret was mounted at a fixed /etc/sw/ssh, so a custom
sftp.hostKeysFolder pointed the server at an empty directory. Mount at
the configured path in both the sftp and all-in-one deployments, and
pin flag/mount agreement in the rendering tests.
This commit is contained in:
Chris Lu
2026-07-21 12:25:21 -07:00
committed by GitHub
parent 68a4e3347f
commit 742bd9f3aa
8 changed files with 259 additions and 10 deletions
@@ -0,0 +1 @@
# Default configuration, kept so chart-testing still installs the chart as-is.
+5
View File
@@ -0,0 +1,5 @@
# SFTP install: the pod only becomes ready if the server loads a host key
# from the generated sftp-ssh-secret, so this exercises the whole path.
sftp:
enabled: true
enableAuth: true
@@ -302,7 +302,7 @@ spec:
{{- end }}
{{- if .Values.allInOne.sftp.enabled }}
- name: config-ssh
mountPath: /etc/sw/ssh
mountPath: {{ .Values.allInOne.sftp.hostKeysFolder | default .Values.sftp.hostKeysFolder | default "/etc/sw/ssh" }}
readOnly: true
{{- if or .Values.allInOne.sftp.enableAuth .Values.sftp.enableAuth }}
- mountPath: /etc/sw/sftp
@@ -176,7 +176,7 @@ spec:
name: config-users
readOnly: true
{{- end }}
- mountPath: /etc/sw/ssh
- mountPath: {{ .Values.sftp.hostKeysFolder | default "/etc/sw/ssh" }}
name: config-ssh
readOnly: true
{{- if and .Values.sftp.authMethods (contains "certificate" .Values.sftp.authMethods) }}
@@ -3,6 +3,15 @@
{{- $admin_pwd := include "seaweedfs.getOrGeneratePassword" (dict "namespace" .Release.Namespace "secretName" $secretName "key" "admin_password" "length" 20) -}}
{{- $read_user_pwd := include "seaweedfs.getOrGeneratePassword" (dict "namespace" .Release.Namespace "secretName" $secretName "key" "readonly_password" "length" 20) -}}
{{- $public_user_pwd := include "seaweedfs.getOrGeneratePassword" (dict "namespace" .Release.Namespace "secretName" $secretName "key" "public_user_password" "length" 20) -}}
{{- $ssh_private_key := "" -}}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace $secretName -}}
{{- if and $existingSecret (index $existingSecret.data "seaweedfs_sftp_ssh_private_key") -}}
{{- $ssh_private_key = index $existingSecret.data "seaweedfs_sftp_ssh_private_key" | b64dec | trim -}}
{{- end -}}
{{/* generate a fresh host key; also replace the key earlier chart versions bundled */}}
{{- if or (not $ssh_private_key) (contains "H4McwcDphteXVullu6q7ephEN1N60z" $ssh_private_key) -}}
{{- $ssh_private_key = genPrivateKey "ed25519" -}}
{{- end -}}
apiVersion: v1
kind: Secret
type: Opaque
@@ -24,11 +33,5 @@ stringData:
public_user_password: {{ $public_user_pwd }}
seaweedfs_sftp_config: '[{"Username":"admin","Password":"{{ $admin_pwd }}","PublicKeys":[],"HomeDir":"/","Permissions":{"/":["read","write","list"]},"Uid":0,"Gid":0},{"Username":"readonly_user","Password":"{{ $read_user_pwd }}","PublicKeys":[],"HomeDir":"/","Permissions":{"/":["read","list"]},"Uid":1112,"Gid":1112},{"Username":"public_user","Password":"{{ $public_user_pwd }}","PublicKeys":[],"HomeDir":"/public","Permissions":{"/public":["write","read","list"]},"Uid":1113,"Gid":1113}]'
seaweedfs_sftp_ssh_private_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDH4McwcDphteXVullu6q7ephEN1N60z+w0qZw0UVW8OwAAAJDjxkmk48ZJ
pAAAAAtzc2gtZWQyNTUxOQAAACDH4McwcDphteXVullu6q7ephEN1N60z+w0qZw0UVW8Ow
AAAEAeVy/4+gf6rjj2jla/AHqJpC1LcS5hn04IUs4q+iVq/MfgxzBwOmG15dW6WW7qrt6m
EQ3U3rTP7DSpnDRRVbw7AAAADHNla291ckAwMDY2NwE=
-----END OPENSSH PRIVATE KEY-----
{{ $ssh_private_key | trim | indent 4 }}
{{- end }}
@@ -0,0 +1,35 @@
{{- if or (and .Values.sftp.enabled (not .Values.sftp.existingSshConfigSecret)) (and .Values.allInOne.enabled .Values.allInOne.sftp.enabled (not (or .Values.allInOne.sftp.existingSshConfigSecret .Values.sftp.existingSshConfigSecret))) }}
{{/* Host keys mounted at sftp.hostKeysFolder; existing keys are kept across upgrades, except the key earlier chart versions bundled. */}}
{{- $secretName := printf "%s-sftp-ssh-secret" (include "seaweedfs.fullname" .) }}
{{- $hostKeys := dict }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace $secretName }}
{{- if $existingSecret }}
{{- range $name, $value := $existingSecret.data }}
{{- if not (contains "H4McwcDphteXVullu6q7ephEN1N60z" (b64dec $value)) }}
{{- $_ := set $hostKeys $name $value }}
{{- end }}
{{- end }}
{{- end }}
{{- if eq (len $hostKeys) 0 }}
{{- $_ := set $hostKeys "ssh_host_ed25519_key" (genPrivateKey "ed25519" | b64enc) }}
{{- end }}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ $secretName }}
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/resource-policy": keep
"helm.sh/hook": "pre-install,pre-upgrade"
labels:
app.kubernetes.io/name: {{ template "seaweedfs.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: sftp
data:
{{- range $name, $value := $hostKeys }}
{{ $name }}: {{ $value }}
{{- end }}
{{- end }}
+1 -1
View File
@@ -1170,7 +1170,7 @@ sftp:
loggingOverrideLevel: null
# SSH server configuration
sshPrivateKey: "/etc/sw/seaweedfs_sftp_ssh_private_key" # Path to the SSH private key file for host authentication
sshPrivateKey: "" # Optional path to a single SSH host key file; the server fails to start if set but missing. Host keys come from hostKeysFolder by default.
hostKeysFolder: "/etc/sw/ssh" # path to folder containing SSH private key files for host authentication
authMethods: "password,publickey" # Comma-separated list of allowed auth methods: password, publickey, certificate
maxAuthTries: 6 # Maximum number of authentication attempts per connection