{encBackend === "vault"
? t("Required for Vault: Secret must contain key 'vault-token'.")
- : t("Not required for Local backend.")}
+ : t("Only used for Vault. Local KMS uses masterKeySecretRef.")}
diff --git a/console-web/i18n/locales/en-US.json b/console-web/i18n/locales/en-US.json
index faf1f48..b426bcd 100755
--- a/console-web/i18n/locales/en-US.json
+++ b/console-web/i18n/locales/en-US.json
@@ -236,7 +236,12 @@
"KMS Secret Name": "KMS Secret Name",
"Secret containing vault-token": "Secret containing vault-token",
"Required for Vault: Secret must contain key 'vault-token'.": "Required for Vault: Secret must contain key 'vault-token'.",
- "Not required for Local backend.": "Not required for Local backend.",
+ "Only used for Vault. Local KMS uses masterKeySecretRef.": "Only used for Vault. Local KMS uses masterKeySecretRef.",
+ "Master Key Secret Name": "Master Key Secret Name",
+ "Master Key Secret Key": "Master Key Secret Key",
+ "Allow insecure development defaults": "Allow insecure development defaults",
+ "Local KMS requires a master key Secret unless insecure development defaults are explicitly enabled.": "Local KMS requires a master key Secret unless insecure development defaults are explicitly enabled.",
+ "Local KMS master key Secret name and key are required": "Local KMS master key Secret name and key are required",
"Failed to load encryption config": "Failed to load encryption config",
"Encryption config updated": "Encryption config updated",
"Loading encryption config...": "Loading encryption config...",
diff --git a/console-web/i18n/locales/zh-CN.json b/console-web/i18n/locales/zh-CN.json
index afd42b6..a36ca4e 100755
--- a/console-web/i18n/locales/zh-CN.json
+++ b/console-web/i18n/locales/zh-CN.json
@@ -233,7 +233,12 @@
"KMS Secret Name": "KMS Secret 名称",
"Secret containing vault-token and TLS certs": "包含 vault-token 和 TLS 证书的 Secret",
"Secret must contain key 'vault-token'. Optional: 'vault-ca-cert', 'vault-client-cert', 'vault-client-key'.": "Secret 必须包含 'vault-token' 键。可选:'vault-ca-cert'、'vault-client-cert'、'vault-client-key'。",
- "Not required for Local backend.": "本地后端不需要 Secret。",
+ "Only used for Vault. Local KMS uses masterKeySecretRef.": "仅用于 Vault。Local KMS 使用 masterKeySecretRef。",
+ "Master Key Secret Name": "Master Key Secret 名称",
+ "Master Key Secret Key": "Master Key Secret 键",
+ "Allow insecure development defaults": "允许开发环境不安全默认值",
+ "Local KMS requires a master key Secret unless insecure development defaults are explicitly enabled.": "Local KMS 需要 master key Secret,除非显式启用开发环境不安全默认值。",
+ "Local KMS master key Secret name and key are required": "Local KMS master key Secret 名称和键不能为空",
"Auth Type": "认证方式",
"Not yet implemented in backend": "后端尚未实现",
"Retry (Seconds)": "重试间隔(秒)",
diff --git a/console-web/types/api.ts b/console-web/types/api.ts
index 4f374d2..cff5cbe 100644
--- a/console-web/types/api.ts
+++ b/console-web/types/api.ts
@@ -396,6 +396,11 @@ export interface VaultInfo {
export interface LocalKmsInfo {
keyDirectory: string | null
+ masterKeySecretRef: {
+ name: string
+ key: string
+ } | null
+ allowInsecureDevDefaults: boolean
}
export interface SecurityContextInfo {
@@ -423,6 +428,11 @@ export interface UpdateEncryptionRequest {
}
local?: {
keyDirectory?: string
+ masterKeySecretRef?: {
+ name: string
+ key: string
+ }
+ allowInsecureDevDefaults?: boolean
}
kmsSecretName?: string
defaultKeyId?: string
diff --git a/deploy/k8s-dev/operator-rbac.yaml b/deploy/k8s-dev/operator-rbac.yaml
index e6a34d7..1768319 100755
--- a/deploy/k8s-dev/operator-rbac.yaml
+++ b/deploy/k8s-dev/operator-rbac.yaml
@@ -42,7 +42,7 @@ rules:
resources: ["tokenreviews"]
verbs: ["create"]
- apiGroups: ["apps"]
- resources: ["statefulsets"]
+ resources: ["statefulsets", "replicasets", "deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["cert-manager.io"]
resources: ["certificates"]
diff --git a/deploy/rustfs-operator/crds/tenant-crd.yaml b/deploy/rustfs-operator/crds/tenant-crd.yaml
index 8e559b8..a6be36c 100644
--- a/deploy/rustfs-operator/crds/tenant-crd.yaml
+++ b/deploy/rustfs-operator/crds/tenant-crd.yaml
@@ -111,10 +111,44 @@ spec:
description: 'Local: optional key directory override.'
nullable: true
properties:
+ allowInsecureDevDefaults:
+ default: false
+ description: |-
+ Explicitly allow RustFS development-only insecure KMS defaults.
+
+ When true, the operator sets `RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true`, allowing
+ Local KMS to start without a master key and store key material as plaintext-dev-only.
+ Do not use this in production.
+ type: boolean
keyDirectory:
- description: 'Absolute directory for KMS key files (default: `/data/kms-keys`).'
+ description: |-
+ Absolute directory for KMS key files.
+
+ Must be in a subdirectory under a RustFS data PVC mount. Defaults to the first data PVC mount
+ under `persistence.path`, for example `/data/rustfs0/.kms-keys`.
nullable: true
type: string
+ masterKeySecretRef:
+ description: |-
+ Secret key selector for `RUSTFS_KMS_LOCAL_MASTER_KEY`.
+
+ Required for Local KMS unless `allowInsecureDevDefaults` is explicitly set to `true`.
+ The referenced Secret key should contain the local master key string used to encrypt
+ Local KMS key files at rest.
+ nullable: true
+ properties:
+ key:
+ description: Secret data key containing the local master key string.
+ minLength: 1
+ type: string
+ name:
+ description: Secret name in the Tenant namespace.
+ minLength: 1
+ type: string
+ required:
+ - key
+ - name
+ type: object
type: object
vault:
description: 'Vault: HTTP(S) endpoint (required when `backend: vault`).'
@@ -127,7 +161,14 @@ spec:
- endpoint
type: object
type: object
+ x-kubernetes-validations:
+ - message: Local KMS requires local.masterKeySecretRef unless local.allowInsecureDevDefaults is true
+ rule: '!self.enabled || self.backend != ''local'' || (has(self.local) && (has(self.local.masterKeySecretRef) || (has(self.local.allowInsecureDevDefaults) && self.local.allowInsecureDevDefaults == true)))'
env:
+ description: |-
+ Additional RustFS container environment variables.
+
+ `RUSTFS_KMS_*` is reserved; configure KMS through `spec.encryption`.
items:
description: EnvVar represents an environment variable present in a Container.
properties:
@@ -426,8 +467,10 @@ spec:
Typical use-case: a StatefulSet Pod gets stuck in Terminating when the node goes down.
Setting this to `ForceDelete` allows the operator to force delete the Pod object so the
StatefulSet controller can recreate it elsewhere.
+ Force deletion requires the Node object to be deleted or marked with an effective
+ `node.kubernetes.io/out-of-service` taint that the target Pod does not tolerate.
- Values: DoNothing | Delete | ForceDelete
+ Values: DoNothing | Delete | ForceDelete | DeleteStatefulSetPod | DeleteDeploymentPod | DeleteBothStatefulSetAndDeploymentPod
enum:
- DoNothing
- Delete
@@ -1725,7 +1768,9 @@ spec:
description: Status of the condition (True, False, Unknown)
type: string
type:
- description: Type of condition (Ready, Progressing, Degraded)
+ description: |-
+ Type of condition (Ready, Reconciling, Degraded, SpecValid, CredentialsReady, KmsReady,
+ TlsReady, PoolsReady, WorkloadsReady, ProvisioningReady)
type: string
required:
- message
diff --git a/deploy/rustfs-operator/crds/tenant.yaml b/deploy/rustfs-operator/crds/tenant.yaml
index 8e559b8..a6be36c 100755
--- a/deploy/rustfs-operator/crds/tenant.yaml
+++ b/deploy/rustfs-operator/crds/tenant.yaml
@@ -111,10 +111,44 @@ spec:
description: 'Local: optional key directory override.'
nullable: true
properties:
+ allowInsecureDevDefaults:
+ default: false
+ description: |-
+ Explicitly allow RustFS development-only insecure KMS defaults.
+
+ When true, the operator sets `RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true`, allowing
+ Local KMS to start without a master key and store key material as plaintext-dev-only.
+ Do not use this in production.
+ type: boolean
keyDirectory:
- description: 'Absolute directory for KMS key files (default: `/data/kms-keys`).'
+ description: |-
+ Absolute directory for KMS key files.
+
+ Must be in a subdirectory under a RustFS data PVC mount. Defaults to the first data PVC mount
+ under `persistence.path`, for example `/data/rustfs0/.kms-keys`.
nullable: true
type: string
+ masterKeySecretRef:
+ description: |-
+ Secret key selector for `RUSTFS_KMS_LOCAL_MASTER_KEY`.
+
+ Required for Local KMS unless `allowInsecureDevDefaults` is explicitly set to `true`.
+ The referenced Secret key should contain the local master key string used to encrypt
+ Local KMS key files at rest.
+ nullable: true
+ properties:
+ key:
+ description: Secret data key containing the local master key string.
+ minLength: 1
+ type: string
+ name:
+ description: Secret name in the Tenant namespace.
+ minLength: 1
+ type: string
+ required:
+ - key
+ - name
+ type: object
type: object
vault:
description: 'Vault: HTTP(S) endpoint (required when `backend: vault`).'
@@ -127,7 +161,14 @@ spec:
- endpoint
type: object
type: object
+ x-kubernetes-validations:
+ - message: Local KMS requires local.masterKeySecretRef unless local.allowInsecureDevDefaults is true
+ rule: '!self.enabled || self.backend != ''local'' || (has(self.local) && (has(self.local.masterKeySecretRef) || (has(self.local.allowInsecureDevDefaults) && self.local.allowInsecureDevDefaults == true)))'
env:
+ description: |-
+ Additional RustFS container environment variables.
+
+ `RUSTFS_KMS_*` is reserved; configure KMS through `spec.encryption`.
items:
description: EnvVar represents an environment variable present in a Container.
properties:
@@ -426,8 +467,10 @@ spec:
Typical use-case: a StatefulSet Pod gets stuck in Terminating when the node goes down.
Setting this to `ForceDelete` allows the operator to force delete the Pod object so the
StatefulSet controller can recreate it elsewhere.
+ Force deletion requires the Node object to be deleted or marked with an effective
+ `node.kubernetes.io/out-of-service` taint that the target Pod does not tolerate.
- Values: DoNothing | Delete | ForceDelete
+ Values: DoNothing | Delete | ForceDelete | DeleteStatefulSetPod | DeleteDeploymentPod | DeleteBothStatefulSetAndDeploymentPod
enum:
- DoNothing
- Delete
@@ -1725,7 +1768,9 @@ spec:
description: Status of the condition (True, False, Unknown)
type: string
type:
- description: Type of condition (Ready, Progressing, Degraded)
+ description: |-
+ Type of condition (Ready, Reconciling, Degraded, SpecValid, CredentialsReady, KmsReady,
+ TlsReady, PoolsReady, WorkloadsReady, ProvisioningReady)
type: string
required:
- message
diff --git a/deploy/rustfs-operator/templates/clusterrole.yaml b/deploy/rustfs-operator/templates/clusterrole.yaml
index bb2bcc9..9235ceb 100755
--- a/deploy/rustfs-operator/templates/clusterrole.yaml
+++ b/deploy/rustfs-operator/templates/clusterrole.yaml
@@ -40,9 +40,9 @@ rules:
resources: ["tokenreviews"]
verbs: ["create"]
- # StatefulSets for tenant pools
+ # StatefulSets for tenant pools and owner lookups for pod cleanup
- apiGroups: ["apps"]
- resources: ["statefulsets"]
+ resources: ["statefulsets", "replicasets", "deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# cert-manager Certificate orchestration and readiness watches
diff --git a/docs/operator-user-guide.md b/docs/operator-user-guide.md
index e042774..e0a7483 100644
--- a/docs/operator-user-guide.md
+++ b/docs/operator-user-guide.md
@@ -345,6 +345,7 @@ The operator reserves these environment variables and manages them automatically
- `RUSTFS_ADDRESS`
- `RUSTFS_CONSOLE_ADDRESS`
- `RUSTFS_CONSOLE_ENABLE`
+- `RUSTFS_KMS_*` variables; use `spec.encryption` instead.
- TLS-related RustFS variables when Tenant TLS is enabled.
For a single-pool single-node single-disk Tenant, `RUSTFS_VOLUMES` is rendered as the local data path, for example `/data/rustfs0`. Multi-pool tenants and other layouts render peer DNS URLs through the Tenant headless Service and are validated by RustFS at runtime.
@@ -358,7 +359,7 @@ For a single-pool single-node single-disk Tenant, `RUSTFS_VOLUMES` is rendered a
- `DeleteDeploymentPod`: Longhorn-compatible force delete for Deployment pods stuck on down nodes.
- `DeleteBothStatefulSetAndDeploymentPod`: Longhorn-compatible force delete for both StatefulSet and Deployment pods.
-Force deletion can have data consistency implications. Use it only when the storage backend and operational procedure are designed for that failure mode.
+Force deletion can have data consistency implications. Use it only when the storage backend and operational procedure are designed for that failure mode. Force deletion requires the Node object to be deleted or marked with an effective `node.kubernetes.io/out-of-service` taint that the target Pod does not tolerate, so volume detach fencing is explicit.
### 7.5 TLS
@@ -425,10 +426,46 @@ Supported backends:
| Backend | Purpose |
|---------|---------|
-| `local` | File-based local KMS key directory. The directory must be absolute, and the Tenant must have exactly one RustFS server replica across all pools. |
+| `local` | File-based local KMS key directory and local master key. The directory must be absolute, must be in a subdirectory under a RustFS data PVC mount, and the Tenant must have exactly one RustFS server replica across all pools. |
| `vault` | HashiCorp Vault endpoint. Requires a Secret containing `vault-token`. |
-Local KMS does not use `kmsSecret`; if you set one, it is ignored. Use Vault KMS for multi-server Tenants.
+Local KMS does not use `kmsSecret`; if you set one, it is ignored. Configure the local master key with `spec.encryption.local.masterKeySecretRef`, which maps to RustFS `RUSTFS_KMS_LOCAL_MASTER_KEY`. By default, Local KMS stores key files under the first data PVC mount, for example `/data/rustfs0/.kms-keys` when `persistence.path` is `/data`. Use Vault KMS for multi-server Tenants. `allowInsecureDevDefaults: true` maps to `RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true` and should only be used for development because RustFS may store Local KMS key material as plaintext-dev-only.
+
+Upgrade note: older operator versions defaulted Local KMS to `/data/kms-keys`, which was not on a data PVC. The operator does not migrate key files automatically. For an existing Local KMS Tenant that still uses the old implicit default, or that explicitly set the legacy path, the operator blocks reconciliation or StatefulSet rollout until you copy any existing key files and `.master-key.salt` into the new PVC-backed directory, keep using the same local master key Secret, then set `spec.encryption.local.keyDirectory` explicitly to that subdirectory.
+
+Local example:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: rustfs-local-kms
+ namespace: storage
+type: Opaque
+stringData:
+ local-master-key: "replace-with-a-strong-local-kms-master-key"
+---
+apiVersion: rustfs.com/v1alpha1
+kind: Tenant
+metadata:
+ name: rustfs-local
+ namespace: storage
+spec:
+ pools:
+ - name: pool-0
+ servers: 1
+ persistence:
+ volumesPerServer: 1
+ encryption:
+ enabled: true
+ backend: local
+ local:
+ keyDirectory: /data/rustfs0/.kms-keys
+ masterKeySecretRef:
+ name: rustfs-local-kms
+ key: local-master-key
+ defaultKeyId: tenant-default
+```
Vault example:
@@ -765,7 +802,7 @@ Common blocked reasons:
| `CredentialSecretNotFound` | Secret exists in the Tenant namespace. |
| `CredentialSecretMissingKey` | Secret contains `accesskey` and `secretkey`. |
| `CredentialSecretTooShort` | Both credential values are at least 8 characters. |
-| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret exists and contains required keys such as `vault-token`. |
+| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret exists and contains required keys, such as Vault `vault-token` or the Local KMS `masterKeySecretRef.key`. |
| `CertManagerCrdMissing` / `CertManagerIssuerNotFound` | cert-manager is installed and the issuer exists. |
| `StatefulSetUpdateValidationFailed` | An immutable StatefulSet or pool-shape field was changed. |
| `ProvisioningFailed` | Check `status.provisioning`, policy ConfigMaps, user Secrets, and RustFS admin credentials. |
diff --git a/docs/operator-user-guide.zh-CN.md b/docs/operator-user-guide.zh-CN.md
index fb511aa..83dd82b 100644
--- a/docs/operator-user-guide.zh-CN.md
+++ b/docs/operator-user-guide.zh-CN.md
@@ -347,6 +347,7 @@ Operator 会自动管理以下环境变量:
- `RUSTFS_ADDRESS`
- `RUSTFS_CONSOLE_ADDRESS`
- `RUSTFS_CONSOLE_ENABLE`
+- `RUSTFS_KMS_*` 变量;请改用 `spec.encryption` 配置
- 启用 TLS 时的 RustFS TLS 相关变量
对于单 pool 的单节点单盘 Tenant,`RUSTFS_VOLUMES` 会渲染为本地数据路径,例如 `/data/rustfs0`。多 pool Tenant 和其他布局仍会通过 Tenant headless Service 渲染 peer DNS URL,并由 RustFS 在运行时校验。
@@ -360,7 +361,7 @@ Operator 会自动管理以下环境变量:
- `DeleteDeploymentPod`:Longhorn 兼容模式,强制删除 down node 上卡住的 Deployment Pod。
- `DeleteBothStatefulSetAndDeploymentPod`:Longhorn 兼容模式,同时处理 StatefulSet 和 Deployment Pod。
-强制删除可能影响数据一致性。只有当存储后端和运维流程明确支持该故障处理方式时才应启用。
+强制删除可能影响数据一致性。只有当存储后端和运维流程明确支持该故障处理方式时才应启用。强制删除要求 Node 对象已删除,或 Node 带有对目标 Pod 生效且未被该 Pod tolerate 的 `node.kubernetes.io/out-of-service` taint,确保 volume detach fencing 是显式的。
### 7.5 TLS
@@ -427,10 +428,46 @@ Tenant 加密通过 `spec.encryption` 配置。
| Backend | 用途 |
|---------|------|
-| `local` | 文件型本地 KMS key 目录。目录必须是绝对路径,且整个 Tenant 所有 pool 的 server 总数必须为 1。 |
+| `local` | 文件型本地 KMS key 目录和本地 master key。目录必须是绝对路径,必须位于 RustFS 数据 PVC mount 下的子目录,且整个 Tenant 所有 pool 的 server 总数必须为 1。 |
| `vault` | HashiCorp Vault endpoint,需要包含 `vault-token` 的 Secret。 |
-Local KMS 不使用 `kmsSecret`;即使设置也会被忽略。多 server Tenant 应使用 Vault KMS。
+Local KMS 不使用 `kmsSecret`;即使设置也会被忽略。请通过 `spec.encryption.local.masterKeySecretRef` 配置本地 master key,它会映射到 RustFS 的 `RUSTFS_KMS_LOCAL_MASTER_KEY`。默认情况下,Local KMS 会把 key 文件存放在第一块数据 PVC mount 下,例如 `persistence.path` 为 `/data` 时使用 `/data/rustfs0/.kms-keys`。多 server Tenant 应使用 Vault KMS。`allowInsecureDevDefaults: true` 会映射到 `RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS=true`,只能用于开发环境,因为 RustFS 可能用 plaintext-dev-only 方式保存 Local KMS key material。
+
+升级提示:旧版本 Operator 的 Local KMS 默认目录是 `/data/kms-keys`,该路径不在数据 PVC 下。Operator 不会自动迁移 key 文件。对于仍使用旧隐式默认值,或显式设置了旧路径的已有 Local KMS Tenant,Operator 会阻断 reconcile 或 StatefulSet 滚动更新;请先把旧目录中的 key 文件和 `.master-key.salt` 复制到新的 PVC 持久化目录,继续使用同一个本地 master key Secret,然后显式设置 `spec.encryption.local.keyDirectory` 为该子目录。
+
+Local 示例:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: rustfs-local-kms
+ namespace: storage
+type: Opaque
+stringData:
+ local-master-key: "replace-with-a-strong-local-kms-master-key"
+---
+apiVersion: rustfs.com/v1alpha1
+kind: Tenant
+metadata:
+ name: rustfs-local
+ namespace: storage
+spec:
+ pools:
+ - name: pool-0
+ servers: 1
+ persistence:
+ volumesPerServer: 1
+ encryption:
+ enabled: true
+ backend: local
+ local:
+ keyDirectory: /data/rustfs0/.kms-keys
+ masterKeySecretRef:
+ name: rustfs-local-kms
+ key: local-master-key
+ defaultKeyId: tenant-default
+```
Vault 示例:
@@ -767,7 +804,7 @@ kubectl logs -n rustfs-system \
| `CredentialSecretNotFound` | Secret 是否存在于 Tenant namespace。 |
| `CredentialSecretMissingKey` | Secret 是否包含 `accesskey` 和 `secretkey`。 |
| `CredentialSecretTooShort` | 两个凭据值是否都至少 8 个字符。 |
-| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret 是否存在,并包含 `vault-token` 等必要 key。 |
+| `KmsSecretNotFound` / `KmsSecretMissingKey` | KMS Secret 是否存在,并包含必要 key,例如 Vault 的 `vault-token` 或 Local KMS 的 `masterKeySecretRef.key`。 |
| `CertManagerCrdMissing` / `CertManagerIssuerNotFound` | cert-manager 是否安装,issuer 是否存在。 |
| `StatefulSetUpdateValidationFailed` | 是否修改了不可变 StatefulSet 字段或 pool 形态字段。 |
| `ProvisioningFailed` | 检查 `status.provisioning`、policy ConfigMap、user Secret 和 RustFS 管理员凭据。 |
diff --git a/src/console/handlers/encryption.rs b/src/console/handlers/encryption.rs
index 0ee3c0e..ad819be 100644
--- a/src/console/handlers/encryption.rs
+++ b/src/console/handlers/encryption.rs
@@ -18,7 +18,7 @@ use crate::console::{
state::Claims,
};
use crate::types::v1alpha1::encryption::{
- EncryptionConfig, KmsBackendType, LocalKmsConfig, VaultKmsConfig,
+ EncryptionConfig, KmsBackendType, LocalKmsConfig, LocalKmsMasterKeySecretRef, VaultKmsConfig,
};
use crate::types::v1alpha1::tenant::Tenant;
use axum::{Extension, Json, extract::Path};
@@ -26,6 +26,12 @@ use k8s_openapi::api::core::v1 as corev1;
use kube::api::{Patch, PatchParams};
use kube::{Api, Client};
+fn trim_to_non_empty(value: Option) -> Option {
+ value
+ .map(|value| value.trim().to_string())
+ .filter(|value| !value.is_empty())
+}
+
/// GET /namespaces/:namespace/tenants/:name/encryption
pub async fn get_encryption(
Path((namespace, name)): Path<(String, String)>,
@@ -49,8 +55,17 @@ pub async fn get_encryption(
}),
local: enc.local.as_ref().map(|l| LocalInfo {
key_directory: l.key_directory.clone(),
+ master_key_secret_ref: l.master_key_secret_ref.as_ref().map(|s| {
+ LocalMasterKeySecretRefInfo {
+ name: s.name.clone(),
+ key: s.key.clone(),
+ }
+ }),
+ allow_insecure_dev_defaults: l.allow_insecure_dev_defaults,
}),
- kms_secret_name: enc.kms_secret.as_ref().map(|s| s.name.clone()),
+ kms_secret_name: (enc.backend == KmsBackendType::Vault)
+ .then(|| enc.kms_secret.as_ref().map(|s| s.name.clone()))
+ .flatten(),
default_key_id: enc.default_key_id.clone(),
security_context: tenant.spec.security_context.as_ref().map(|sc| {
SecurityContextInfo {
@@ -101,50 +116,65 @@ pub async fn update_encryption(
Some("vault") => KmsBackendType::Vault,
_ => KmsBackendType::Local,
};
+ let vault_endpoint = body
+ .vault
+ .as_ref()
+ .and_then(|v| trim_to_non_empty(Some(v.endpoint.clone())));
+ let kms_secret_name = trim_to_non_empty(body.kms_secret_name.clone());
+ let default_key_id = trim_to_non_empty(body.default_key_id.clone());
if backend == KmsBackendType::Vault {
- let vault_ok = body
- .vault
- .as_ref()
- .map(|v| !v.endpoint.is_empty())
- .unwrap_or(false);
- if !vault_ok {
+ if vault_endpoint.is_none() {
return Err(Error::BadRequest {
message: "Vault backend requires vault.endpoint to be non-empty".to_string(),
});
}
- let secret_ok = body
- .kms_secret_name
- .as_ref()
- .map(|s| !s.is_empty())
- .unwrap_or(false);
- if !secret_ok {
+ if kms_secret_name.is_none() {
return Err(Error::BadRequest {
message: "Vault backend requires kmsSecretName".to_string(),
});
}
+ } else {
+ let local = body.local.as_ref();
+ let allow_insecure_dev_defaults = local
+ .and_then(|l| l.allow_insecure_dev_defaults)
+ .unwrap_or(false);
+ let master_key_ref_ok = local
+ .and_then(|l| l.master_key_secret_ref.as_ref())
+ .is_some_and(|s| !s.name.trim().is_empty() && !s.key.trim().is_empty());
+ if !allow_insecure_dev_defaults && !master_key_ref_ok {
+ return Err(Error::BadRequest {
+ message: "Local backend requires local.masterKeySecretRef.name/key unless local.allowInsecureDevDefaults is true".to_string(),
+ });
+ }
}
let vault = if backend == KmsBackendType::Vault {
- body.vault.map(|v| VaultKmsConfig {
- endpoint: v.endpoint,
- })
+ vault_endpoint.map(|endpoint| VaultKmsConfig { endpoint })
} else {
None
};
let local = if backend == KmsBackendType::Local {
body.local.map(|l| LocalKmsConfig {
- key_directory: l.key_directory,
+ key_directory: trim_to_non_empty(l.key_directory),
+ master_key_secret_ref: l.master_key_secret_ref.map(|s| {
+ LocalKmsMasterKeySecretRef {
+ name: s.name.trim().to_string(),
+ key: s.key.trim().to_string(),
+ }
+ }),
+ allow_insecure_dev_defaults: l.allow_insecure_dev_defaults.unwrap_or(false),
})
} else {
None
};
- let kms_secret = body
- .kms_secret_name
- .filter(|s| !s.is_empty())
- .map(|s| corev1::LocalObjectReference { name: s });
+ let kms_secret = if backend == KmsBackendType::Vault {
+ kms_secret_name.map(|s| corev1::LocalObjectReference { name: s })
+ } else {
+ None
+ };
Some(EncryptionConfig {
enabled: true,
@@ -152,7 +182,7 @@ pub async fn update_encryption(
vault,
local,
kms_secret,
- default_key_id: body.default_key_id.filter(|s| !s.is_empty()),
+ default_key_id,
})
} else {
Some(EncryptionConfig {
@@ -190,3 +220,18 @@ async fn create_client(claims: &Claims) -> Result {
message: format!("Failed to create K8s client: {}", e),
})
}
+
+#[cfg(test)]
+mod tests {
+ use super::trim_to_non_empty;
+
+ #[test]
+ fn trim_to_non_empty_drops_blank_strings() {
+ assert_eq!(trim_to_non_empty(None), None);
+ assert_eq!(trim_to_non_empty(Some(" ".to_string())), None);
+ assert_eq!(
+ trim_to_non_empty(Some(" /data/rustfs0/.kms-keys ".to_string())),
+ Some("/data/rustfs0/.kms-keys".to_string())
+ );
+ }
+}
diff --git a/src/console/models/encryption.rs b/src/console/models/encryption.rs
index f2865dc..2c4f3b4 100644
--- a/src/console/models/encryption.rs
+++ b/src/console/models/encryption.rs
@@ -39,6 +39,15 @@ pub struct VaultInfo {
#[serde(rename_all = "camelCase")]
pub struct LocalInfo {
pub key_directory: Option,
+ pub master_key_secret_ref: Option,
+ pub allow_insecure_dev_defaults: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct LocalMasterKeySecretRefInfo {
+ pub name: String,
+ pub key: String,
}
/// SecurityContext information (TenantSpec; shown alongside encryption for convenience).
@@ -73,6 +82,8 @@ pub struct UpdateVaultRequest {
#[serde(rename_all = "camelCase")]
pub struct UpdateLocalRequest {
pub key_directory: Option,
+ pub master_key_secret_ref: Option,
+ pub allow_insecure_dev_defaults: Option,
}
#[derive(Debug, Deserialize, ToSchema)]
diff --git a/src/context.rs b/src/context.rs
index de39bc0..940285d 100755
--- a/src/context.rs
+++ b/src/context.rs
@@ -13,6 +13,7 @@
// limitations under the License.
use crate::types;
+use crate::types::v1alpha1::encryption::LocalKmsMasterKeySecretRef;
use crate::types::v1alpha1::tenant::Tenant;
use k8s_openapi::NamespaceResourceScope;
use k8s_openapi::api::core::v1::Secret;
@@ -24,6 +25,7 @@ use serde::de::DeserializeOwned;
use snafu::Snafu;
use snafu::futures::TryFutureExt;
use std::fmt::Debug;
+use std::path::{Component, Path, PathBuf};
use tracing::info;
#[derive(Debug, Snafu)]
@@ -75,14 +77,110 @@ pub enum Error {
Serde { source: serde_json::Error },
}
-/// Validates Local KMS: absolute `keyDirectory` and at most one server replica across pools.
+fn local_kms_key_directory(
+ local: Option<&types::v1alpha1::encryption::LocalKmsConfig>,
+ pools: &[types::v1alpha1::pool::Pool],
+) -> String {
+ local
+ .and_then(|l| l.key_directory.clone())
+ .unwrap_or_else(|| {
+ let base_path = pools
+ .first()
+ .and_then(|pool| pool.persistence.path.as_deref());
+ types::v1alpha1::persistence::default_local_kms_key_directory(base_path)
+ })
+}
+
+fn normalize_absolute_path(path: &str) -> Option {
+ let path = Path::new(path);
+ if !path.is_absolute() {
+ return None;
+ }
+
+ let mut normalized = PathBuf::from("/");
+ for component in path.components() {
+ match component {
+ Component::RootDir | Component::CurDir => {}
+ Component::Normal(part) => normalized.push(part),
+ Component::ParentDir => {
+ normalized.pop();
+ }
+ Component::Prefix(_) => return None,
+ }
+ }
+
+ Some(normalized)
+}
+
+fn local_kms_key_dir_is_on_data_volume(
+ key_dir: &str,
+ pools: &[types::v1alpha1::pool::Pool],
+) -> bool {
+ let Some(key_dir) = normalize_absolute_path(key_dir) else {
+ return false;
+ };
+
+ pools.iter().any(|pool| {
+ (0..pool.persistence.volumes_per_server).any(|shard| {
+ let mount_path = types::v1alpha1::persistence::data_volume_mount_path(
+ pool.persistence.path.as_deref(),
+ shard,
+ );
+ normalize_absolute_path(&mount_path)
+ .is_some_and(|mount_path| key_dir != mount_path && key_dir.starts_with(mount_path))
+ })
+ })
+}
+
+fn local_kms_data_volume_hint(pools: &[types::v1alpha1::pool::Pool]) -> String {
+ pools
+ .first()
+ .map(|pool| {
+ types::v1alpha1::persistence::default_local_kms_key_directory(
+ pool.persistence.path.as_deref(),
+ )
+ })
+ .unwrap_or_else(|| types::v1alpha1::persistence::default_local_kms_key_directory(None))
+}
+
+fn local_kms_legacy_path_migration_message(
+ key_dir: &str,
+ pools: &[types::v1alpha1::pool::Pool],
+) -> String {
+ let target_dir = local_kms_data_volume_hint(pools);
+ format!(
+ "Local KMS keyDirectory '{}' is the legacy non-PVC path. Copy existing key files and .master-key.salt into '{}', then set spec.encryption.local.keyDirectory to that PVC-backed subdirectory before rolling RustFS pods.",
+ key_dir, target_dir
+ )
+}
+
+fn reserved_kms_env_var_name(tenant: &Tenant) -> Option<&str> {
+ tenant
+ .spec
+ .env
+ .iter()
+ .find(|env| env.name.starts_with("RUSTFS_KMS_"))
+ .map(|env| env.name.as_str())
+}
+
+fn validate_no_reserved_kms_env(tenant: &Tenant) -> Result<(), Error> {
+ if let Some(env_name) = reserved_kms_env_var_name(tenant) {
+ return Err(Error::KmsConfigInvalid {
+ message: format!(
+ "spec.env must not set reserved KMS environment variable '{}'. Configure KMS through spec.encryption so the operator can validate persistence, migration, and Secret references.",
+ env_name
+ ),
+ });
+ }
+ Ok(())
+}
+
+/// Validates Local KMS: absolute persistent `keyDirectory` and at most one server replica across pools.
fn validate_local_kms_tenant(
local: Option<&types::v1alpha1::encryption::LocalKmsConfig>,
pools: &[types::v1alpha1::pool::Pool],
) -> Result<(), Error> {
- let key_dir = local
- .and_then(|l| l.key_directory.as_deref())
- .unwrap_or("/data/kms-keys");
+ let key_dir = local_kms_key_directory(local, pools);
if !key_dir.starts_with('/') {
return Err(Error::KmsConfigInvalid {
message: format!(
@@ -91,6 +189,20 @@ fn validate_local_kms_tenant(
),
});
}
+ if key_dir == types::v1alpha1::persistence::LEGACY_LOCAL_KMS_KEY_DIR {
+ return Err(Error::KmsConfigInvalid {
+ message: local_kms_legacy_path_migration_message(&key_dir, pools),
+ });
+ }
+ if !local_kms_key_dir_is_on_data_volume(&key_dir, pools) {
+ return Err(Error::KmsConfigInvalid {
+ message: format!(
+ "Local KMS keyDirectory must be in a subdirectory under a RustFS data PVC mount (got \"{}\"). Use the default or a path such as \"{}\".",
+ key_dir,
+ local_kms_data_volume_hint(pools)
+ ),
+ });
+ }
let total_servers: i32 = pools.iter().map(|p| p.servers).sum();
if total_servers > 1 {
return Err(Error::KmsConfigInvalid {
@@ -100,6 +212,65 @@ fn validate_local_kms_tenant(
Ok(())
}
+fn validate_local_kms_master_key_ref(
+ local: Option<&types::v1alpha1::encryption::LocalKmsConfig>,
+) -> Result