diff --git a/deploy/rustfs-operator/README.md b/deploy/rustfs-operator/README.md index 74f4e9f..a1eb1e2 100755 --- a/deploy/rustfs-operator/README.md +++ b/deploy/rustfs-operator/README.md @@ -89,6 +89,25 @@ Operator STS does not present a client certificate when calling the Tenant. Tena When `operator.serviceMonitor.enabled=true`, the chart creates scrape targets for both the operator observability endpoint and the Console API `/metrics` endpoint. +### Tenant RPC Authentication + +Use `spec.rpcSecret` to keep RustFS internode RPC authentication independent from +the administrator credentials in `spec.credsSecret`: + +```yaml +spec: + credsSecret: + name: rustfs-admin-creds + rpcSecret: + name: rustfs-rpc-auth + key: rpc-secret +``` + +The operator maps the selected Secret key to `RUSTFS_RPC_SECRET` in every RustFS +Pod. Keep this value stable while rotating administrator credentials. If +`spec.rpcSecret` is omitted, the operator does not set `RUSTFS_RPC_SECRET` and +RustFS resolves the RPC secret from its own credential configuration. + ### Tenant Provisioning Tenants can declare RustFS canned policies, regular users, and buckets directly in `spec.policies`, `spec.users`, and `spec.buckets`. Provisioning starts only after the Tenant workload is ready, uses `spec.credsSecret` as the RustFS admin credential source, and reports progress under `status.provisioning`. diff --git a/deploy/rustfs-operator/crds/tenant-crd.yaml b/deploy/rustfs-operator/crds/tenant-crd.yaml index fceeaa0..436ea80 100644 --- a/deploy/rustfs-operator/crds/tenant-crd.yaml +++ b/deploy/rustfs-operator/crds/tenant-crd.yaml @@ -1410,6 +1410,26 @@ spec: priorityClassName: nullable: true type: string + rpcSecret: + description: |- + Optional Secret key selector for RustFS internode RPC authentication. + + When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every + RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the + operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own + credential configuration. + nullable: true + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object scheduler: nullable: true type: string diff --git a/deploy/rustfs-operator/crds/tenant.yaml b/deploy/rustfs-operator/crds/tenant.yaml index fceeaa0..436ea80 100755 --- a/deploy/rustfs-operator/crds/tenant.yaml +++ b/deploy/rustfs-operator/crds/tenant.yaml @@ -1410,6 +1410,26 @@ spec: priorityClassName: nullable: true type: string + rpcSecret: + description: |- + Optional Secret key selector for RustFS internode RPC authentication. + + When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every + RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the + operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own + credential configuration. + nullable: true + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object scheduler: nullable: true type: string diff --git a/src/types/v1alpha1.rs b/src/types/v1alpha1.rs index 5ebf2c8..36d11f6 100755 --- a/src/types/v1alpha1.rs +++ b/src/types/v1alpha1.rs @@ -154,6 +154,20 @@ mod tenant_provisioning_crd_tests { let spec = &schema["properties"]["spec"]; let status = &schema["properties"]["status"]; + assert_eq!(spec["properties"]["rpcSecret"]["type"], json!("object")); + assert_eq!( + spec["properties"]["rpcSecret"]["properties"]["name"]["minLength"], + json!(1) + ); + assert_eq!( + spec["properties"]["rpcSecret"]["properties"]["key"]["minLength"], + json!(1) + ); + assert_eq!( + spec["properties"]["rpcSecret"]["required"], + json!(["key", "name"]) + ); + assert_eq!(spec["properties"]["policies"]["type"], json!("array")); assert_eq!(spec["properties"]["users"]["type"], json!("array")); assert_eq!(spec["properties"]["buckets"]["type"], json!("array")); diff --git a/src/types/v1alpha1/tenant.rs b/src/types/v1alpha1/tenant.rs index dd6f1e3..4fea7b6 100755 --- a/src/types/v1alpha1/tenant.rs +++ b/src/types/v1alpha1/tenant.rs @@ -39,6 +39,17 @@ pub(crate) const MAX_TENANT_POLICIES: u32 = 256; pub(crate) const MAX_TENANT_USERS: u32 = 256; pub(crate) const MAX_TENANT_BUCKETS: u32 = 1024; +/// Reference to one key in a namespaced Kubernetes Secret. +#[derive(Deserialize, Serialize, Clone, Debug, KubeSchema, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RpcSecretRef { + #[schemars(length(min = 1))] + pub name: String, + + #[schemars(length(min = 1))] + pub key: String, +} + #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, KubeSchema, Default)] #[kube( group = "rustfs.com", @@ -169,6 +180,15 @@ pub struct TenantSpec { #[serde(default, skip_serializing_if = "Option::is_none")] pub creds_secret: Option, + /// Optional Secret key selector for RustFS internode RPC authentication. + /// + /// When configured, the operator maps this key to `RUSTFS_RPC_SECRET` for every + /// RustFS Pod. Keep it stable while rotating `credsSecret`. When omitted, the + /// operator does not set `RUSTFS_RPC_SECRET`; RustFS resolves it from its own + /// credential configuration. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rpc_secret: Option, + /// Canned policies that should be applied to the RustFS tenant. #[schemars( length(max = MAX_TENANT_POLICIES), diff --git a/src/types/v1alpha1/tenant/workloads.rs b/src/types/v1alpha1/tenant/workloads.rs index 39f2ee8..640b69d 100755 --- a/src/types/v1alpha1/tenant/workloads.rs +++ b/src/types/v1alpha1/tenant/workloads.rs @@ -30,6 +30,7 @@ const LOCAL_KMS_KEY_DIR_ENV: &str = "RUSTFS_KMS_KEY_DIR"; const LOCAL_KMS_LOCAL_KEY_DIR_ENV: &str = "RUSTFS_KMS_LOCAL_KEY_DIR"; const LOCAL_KMS_MASTER_KEY_ENV: &str = "RUSTFS_KMS_LOCAL_MASTER_KEY"; const KMS_ALLOW_INSECURE_DEV_DEFAULTS_ENV: &str = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS"; +const RPC_SECRET_ENV: &str = "RUSTFS_RPC_SECRET"; const VOLUME_CLAIM_TEMPLATE_PREFIX: &str = "vol"; const DEFAULT_RUN_AS_USER: i64 = 10001; const DEFAULT_RUN_AS_GROUP: i64 = 10001; @@ -522,6 +523,24 @@ impl Tenant { }); } + // Keep internode RPC authentication independent from admin credentials when + // the Tenant explicitly selects a dedicated Secret key. If omitted, RustFS + // retains ownership of RPC secret resolution. + if let Some(ref secret_ref) = self.spec.rpc_secret { + env_vars.push(corev1::EnvVar { + name: RPC_SECRET_ENV.to_owned(), + value_from: Some(corev1::EnvVarSource { + secret_key_ref: Some(corev1::SecretKeySelector { + name: secret_ref.name.clone(), + key: secret_ref.key.clone(), + optional: Some(false), + }), + ..Default::default() + }), + ..Default::default() + }); + } + // Merge with user-provided environment variables. // Preserve the legacy override behavior except for operator-managed runtime // values that must stay aligned with rendered mounts, probes, status, and hash. @@ -532,6 +551,9 @@ impl Tenant { if is_kms_operator_managed_env_var(&user_env.name) { continue; } + if self.spec.rpc_secret.is_some() && user_env.name == RPC_SECRET_ENV { + continue; + } // Remove any existing var with the same name to allow non-reserved overrides. env_vars.retain(|e| e.name != user_env.name); env_vars.push(user_env.clone()); @@ -1121,6 +1143,7 @@ mod tests { EncryptionConfig, KmsBackendType, LocalKmsConfig, LocalKmsMasterKeySecretRef, }; use crate::types::v1alpha1::logging::{LoggingConfig, LoggingMode}; + use crate::types::v1alpha1::tenant::RpcSecretRef; use crate::types::v1alpha1::tls::{SecretKeyReference, TlsPlan}; use k8s_openapi::api::core::v1 as corev1; @@ -1233,6 +1256,86 @@ mod tests { })); } + #[test] + fn omitted_rpc_secret_leaves_rpc_auth_resolution_to_rustfs() { + let tenant = crate::tests::create_test_tenant(None, None); + let pool = &tenant.spec.pools[0]; + + let statefulset = tenant + .new_statefulset(pool) + .expect("Should create StatefulSet without an RPC Secret"); + let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0]; + + assert!( + container + .env + .as_ref() + .is_none_or(|env| env.iter().all(|var| var.name != "RUSTFS_RPC_SECRET")) + ); + } + + #[test] + fn rpc_secret_maps_selected_secret_key_and_owns_the_env_var() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.spec.rpc_secret = Some(RpcSecretRef { + name: "tenant-rpc-auth".to_string(), + key: "rpc-secret".to_string(), + }); + tenant.spec.env.push(corev1::EnvVar { + name: "RUSTFS_RPC_SECRET".to_string(), + value: Some("raw-override-must-not-win".to_string()), + ..Default::default() + }); + let pool = &tenant.spec.pools[0]; + + let statefulset = tenant + .new_statefulset(pool) + .expect("Should create StatefulSet with an RPC Secret"); + let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0]; + let rpc_env = container + .env + .as_ref() + .unwrap() + .iter() + .filter(|var| var.name == "RUSTFS_RPC_SECRET") + .collect::>(); + + assert_eq!(rpc_env.len(), 1); + assert_eq!(rpc_env[0].value, None); + assert_eq!( + rpc_env[0] + .value_from + .as_ref() + .and_then(|source| source.secret_key_ref.as_ref()), + Some(&corev1::SecretKeySelector { + name: "tenant-rpc-auth".to_string(), + key: "rpc-secret".to_string(), + optional: Some(false), + }) + ); + } + + #[test] + fn raw_rpc_secret_env_remains_supported_without_rpc_secret_ref() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.spec.env.push(corev1::EnvVar { + name: "RUSTFS_RPC_SECRET".to_string(), + value: Some("legacy-explicit-rpc-secret".to_string()), + ..Default::default() + }); + let pool = &tenant.spec.pools[0]; + + let statefulset = tenant + .new_statefulset(pool) + .expect("Should preserve the legacy raw RPC Secret environment variable"); + let container = &statefulset.spec.unwrap().template.spec.unwrap().containers[0]; + + assert_eq!( + env_value(container, "RUSTFS_RPC_SECRET"), + Some("legacy-explicit-rpc-secret") + ); + } + #[test] fn cert_manager_tls_statefulset_maps_secret_to_rustfs_tls_files() { let tenant = crate::tests::create_test_tenant(None, None); @@ -2439,6 +2542,30 @@ mod tests { ); } + #[test] + fn test_statefulset_rpc_secret_change_detected() { + let mut tenant = crate::tests::create_test_tenant(None, None); + tenant.spec.rpc_secret = Some(RpcSecretRef { + name: "tenant-rpc-auth".to_string(), + key: "rpc-secret".to_string(), + }); + let pool = &tenant.spec.pools[0]; + let statefulset = tenant + .new_statefulset(pool) + .expect("Should create StatefulSet"); + + tenant.spec.rpc_secret.as_mut().unwrap().name = "rotated-rpc-auth".to_string(); + + let needs_update = tenant + .statefulset_needs_update(&statefulset, pool) + .expect("Should check update need"); + + assert!( + needs_update, + "StatefulSet should need update when the RPC Secret reference changes" + ); + } + // Test: StatefulSet diff detection - resources change #[test] fn test_statefulset_resources_change_detected() {