diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index d0f26d7d8..1921ec9de 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -47,6 +47,8 @@ pub struct ConfigureLocalKmsRequest { pub max_cached_keys: Option, /// Cache TTL in seconds pub cache_ttl_seconds: Option, + /// Allow development-only insecure defaults + pub allow_insecure_dev_defaults: Option, } impl fmt::Debug for ConfigureLocalKmsRequest { @@ -62,6 +64,7 @@ impl fmt::Debug for ConfigureLocalKmsRequest { .field("enable_cache", &self.enable_cache) .field("max_cached_keys", &self.max_cached_keys) .field("cache_ttl_seconds", &self.cache_ttl_seconds) + .field("allow_insecure_dev_defaults", &self.allow_insecure_dev_defaults) .finish() } } @@ -95,6 +98,8 @@ pub struct ConfigureVaultKmsRequest { pub max_cached_keys: Option, /// Cache TTL in seconds pub cache_ttl_seconds: Option, + /// Allow development-only insecure defaults + pub allow_insecure_dev_defaults: Option, } /// Request to configure KMS with Vault Transit backend @@ -122,6 +127,8 @@ pub struct ConfigureVaultTransitKmsRequest { pub max_cached_keys: Option, /// Cache TTL in seconds pub cache_ttl_seconds: Option, + /// Allow development-only insecure defaults + pub allow_insecure_dev_defaults: Option, } /// Generic KMS configuration request @@ -351,6 +358,7 @@ impl ConfigureLocalKmsRequest { master_key: self.master_key.clone(), file_permissions: self.file_permissions, }), + allow_insecure_dev_defaults: self.allow_insecure_dev_defaults.unwrap_or(false), timeout: Duration::from_secs(self.timeout_seconds.unwrap_or(30)), retry_attempts: self.retry_attempts.unwrap_or(3), enable_cache: self.enable_cache.unwrap_or(true), @@ -387,6 +395,7 @@ impl ConfigureVaultKmsRequest { None }, })), + allow_insecure_dev_defaults: self.allow_insecure_dev_defaults.unwrap_or(false), timeout: Duration::from_secs(self.timeout_seconds.unwrap_or(30)), retry_attempts: self.retry_attempts.unwrap_or(3), enable_cache: self.enable_cache.unwrap_or(true), @@ -421,6 +430,7 @@ impl ConfigureVaultTransitKmsRequest { None }, })), + allow_insecure_dev_defaults: self.allow_insecure_dev_defaults.unwrap_or(false), timeout: Duration::from_secs(self.timeout_seconds.unwrap_or(30)), retry_attempts: self.retry_attempts.unwrap_or(3), enable_cache: self.enable_cache.unwrap_or(true), @@ -509,6 +519,53 @@ mod tests { assert_eq!(config.backend, KmsBackend::Local); } + #[test] + fn test_configure_request_development_defaults_require_opt_in() { + let local_raw = serde_json::json!({ + "backend_type": "local", + "key_dir": "/tmp/kms-key-dir" + }); + let request: ConfigureKmsRequest = serde_json::from_value(local_raw).expect("local request should deserialize"); + let config = request.to_kms_config(); + assert!(config.validate().is_err()); + + let local_opt_in_raw = serde_json::json!({ + "backend_type": "local", + "key_dir": "/tmp/kms-key-dir", + "allow_insecure_dev_defaults": true + }); + let request: ConfigureKmsRequest = serde_json::from_value(local_opt_in_raw).expect("local request should deserialize"); + assert!(request.to_kms_config().validate().is_ok()); + + let vault_raw = serde_json::json!({ + "backend_type": "vault", + "address": "http://127.0.0.1:8200", + "auth_method": { + "Token": { + "token": "dev-token" + } + }, + "skip_tls_verify": true + }); + let request: ConfigureKmsRequest = serde_json::from_value(vault_raw).expect("vault request should deserialize"); + let config = request.to_kms_config(); + assert!(config.validate().is_err()); + + let vault_opt_in_raw = serde_json::json!({ + "backend_type": "vault", + "address": "http://127.0.0.1:8200", + "auth_method": { + "Token": { + "token": "dev-token" + } + }, + "skip_tls_verify": true, + "allow_insecure_dev_defaults": true + }); + let request: ConfigureKmsRequest = serde_json::from_value(vault_opt_in_raw).expect("vault request should deserialize"); + assert!(request.to_kms_config().validate().is_ok()); + } + #[test] fn test_vault_transit_summary_reports_backend_details() { let config = KmsConfig { @@ -523,6 +580,7 @@ mod tests { mount_path: "transit".to_string(), tls: None, })), + allow_insecure_dev_defaults: true, timeout: Duration::from_secs(30), retry_attempts: 3, enable_cache: true, @@ -569,6 +627,7 @@ mod tests { enable_cache: Some(true), max_cached_keys: Some(16), cache_ttl_seconds: Some(60), + allow_insecure_dev_defaults: None, }); let vault = ConfigureKmsRequest::VaultTransit(ConfigureVaultTransitKmsRequest { address: "https://vault.example.com:8200".to_string(), @@ -584,6 +643,7 @@ mod tests { enable_cache: None, max_cached_keys: None, cache_ttl_seconds: None, + allow_insecure_dev_defaults: None, }); let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest { address: "https://vault.example.com:8200".to_string(), @@ -602,6 +662,7 @@ mod tests { enable_cache: None, max_cached_keys: None, cache_ttl_seconds: None, + allow_insecure_dev_defaults: None, }); let rendered = format!("{local:?}\n{vault:?}\n{approle:?}"); diff --git a/crates/kms/src/backends/local.rs b/crates/kms/src/backends/local.rs index f378790f8..2d8d76fd6 100644 --- a/crates/kms/src/backends/local.rs +++ b/crates/kms/src/backends/local.rs @@ -564,6 +564,8 @@ pub struct LocalKmsBackend { impl LocalKmsBackend { /// Create a new LocalKmsBackend pub async fn new(config: KmsConfig) -> Result { + config.validate()?; + let local_config = match &config.backend_config { crate::config::BackendConfig::Local(local_config) => local_config.clone(), crate::config::BackendConfig::VaultKv2(_) | crate::config::BackendConfig::VaultTransit(_) => { diff --git a/crates/kms/src/backends/vault.rs b/crates/kms/src/backends/vault.rs index fe04afae0..f3dce6a72 100644 --- a/crates/kms/src/backends/vault.rs +++ b/crates/kms/src/backends/vault.rs @@ -595,6 +595,8 @@ pub struct VaultKmsBackend { impl VaultKmsBackend { /// Create a new VaultKmsBackend pub async fn new(config: KmsConfig) -> Result { + config.validate()?; + let vault_config = match &config.backend_config { crate::config::BackendConfig::VaultKv2(vault_config) => (**vault_config).clone(), crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::VaultTransit(_) => { diff --git a/crates/kms/src/backends/vault_transit.rs b/crates/kms/src/backends/vault_transit.rs index 720aebc05..0d7130826 100644 --- a/crates/kms/src/backends/vault_transit.rs +++ b/crates/kms/src/backends/vault_transit.rs @@ -483,6 +483,8 @@ pub struct VaultTransitKmsBackend { impl VaultTransitKmsBackend { pub async fn new(config: KmsConfig) -> Result { + config.validate()?; + let vault_config = match &config.backend_config { crate::config::BackendConfig::VaultTransit(vault_config) => (**vault_config).clone(), crate::config::BackendConfig::VaultKv2(vault_config) => VaultTransitConfig { diff --git a/crates/kms/src/config.rs b/crates/kms/src/config.rs index 194887ff5..c133200fc 100644 --- a/crates/kms/src/config.rs +++ b/crates/kms/src/config.rs @@ -19,10 +19,13 @@ use rustfs_security_governance::{RedactionLevel, RedactionRule}; use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str}; use serde::{Deserialize, Serialize}; use std::fmt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Duration; use url::Url; +pub const ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS: &str = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS"; +pub const ENV_KMS_VAULT_SKIP_TLS_VERIFY: &str = "RUSTFS_KMS_VAULT_SKIP_TLS_VERIFY"; + pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[ RedactionRule::new("kms.local.master_key", RedactionLevel::Secret, "local backend key encryption material"), RedactionRule::new("kms.vault.token", RedactionLevel::Secret, "vault authentication token"), @@ -93,6 +96,9 @@ pub struct KmsConfig { pub default_key_id: Option, /// Backend-specific configuration pub backend_config: BackendConfig, + /// Allow development-only insecure defaults such as plaintext local keys or HTTP Vault. + #[serde(default)] + pub allow_insecure_dev_defaults: bool, /// Operation timeout pub timeout: Duration, /// Number of retry attempts @@ -109,6 +115,7 @@ impl Default for KmsConfig { backend: KmsBackend::default(), default_key_id: None, backend_config: BackendConfig::default(), + allow_insecure_dev_defaults: false, timeout: Duration::from_secs(30), retry_attempts: 3, enable_cache: true, @@ -405,6 +412,12 @@ impl KmsConfig { self } + /// Explicitly allow development-only KMS defaults. + pub fn with_insecure_development_defaults(mut self) -> Self { + self.allow_insecure_dev_defaults = true; + self + } + /// Set operation timeout pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; @@ -435,12 +448,30 @@ impl KmsConfig { if !config.key_dir.is_absolute() { return Err(KmsError::configuration_error("Local key directory must be an absolute path")); } + + if !self.allow_insecure_dev_defaults { + if config.master_key.as_deref().is_none_or(str::is_empty) { + return Err(development_default_error( + "Local KMS requires a master key outside explicit development mode", + )); + } + + if is_under_temp_dir(&config.key_dir) { + return Err(development_default_error( + "Local KMS key directory must not be under the process temp directory outside explicit development mode", + )); + } + } } BackendConfig::VaultKv2(config) => { if !config.address.starts_with("http://") && !config.address.starts_with("https://") { return Err(KmsError::configuration_error("Vault KV2 address must use http or https scheme")); } + if !self.allow_insecure_dev_defaults { + validate_vault_development_defaults("Vault KV2", &config.address, &config.auth_method, config.tls.as_ref())?; + } + if config.mount_path.is_empty() { return Err(KmsError::configuration_error("Vault KV2 mount path cannot be empty")); } @@ -461,6 +492,15 @@ impl KmsConfig { return Err(KmsError::configuration_error("Vault Transit address must use http or https scheme")); } + if !self.allow_insecure_dev_defaults { + validate_vault_development_defaults( + "Vault Transit", + &config.address, + &config.auth_method, + config.tls.as_ref(), + )?; + } + if config.mount_path.is_empty() { return Err(KmsError::configuration_error("Vault Transit mount path cannot be empty")); } @@ -520,6 +560,8 @@ impl KmsConfig { // Enable cache config.enable_cache = get_env_bool("RUSTFS_KMS_ENABLE_CACHE", config.enable_cache); + config.allow_insecure_dev_defaults = + get_env_bool(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, config.allow_insecure_dev_defaults); // Backend-specific configuration match config.backend { @@ -536,6 +578,7 @@ impl KmsConfig { KmsBackend::VaultKv2 => { let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200"); let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token"); + let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false); config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig { address, @@ -544,19 +587,20 @@ impl KmsConfig { mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"), kv_mount: get_env_str("RUSTFS_KMS_VAULT_KV_MOUNT", "secret"), key_path_prefix: get_env_str("RUSTFS_KMS_VAULT_KEY_PREFIX", "rustfs/kms/keys"), - tls: None, + tls: vault_tls_config(skip_tls_verify), })); } KmsBackend::VaultTransit => { let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200"); let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token"); + let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false); config.backend_config = BackendConfig::VaultTransit(Box::new(VaultTransitConfig { address, auth_method: VaultAuthMethod::Token { token }, namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"), mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"), - tls: None, + tls: vault_tls_config(skip_tls_verify), })); } } @@ -566,6 +610,50 @@ impl KmsConfig { } } +fn vault_tls_config(skip_tls_verify: bool) -> Option { + skip_tls_verify.then_some(TlsConfig { + ca_cert_path: None, + client_cert_path: None, + client_key_path: None, + skip_verify: true, + }) +} + +fn development_default_error(reason: &str) -> KmsError { + KmsError::configuration_error(format!("{reason}; set {ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS}=true only for development")) +} + +fn is_under_temp_dir(path: &Path) -> bool { + path.starts_with(std::env::temp_dir()) +} + +fn validate_vault_development_defaults( + backend_name: &str, + address: &str, + auth_method: &VaultAuthMethod, + tls: Option<&TlsConfig>, +) -> Result<()> { + if address.starts_with("http://") { + return Err(development_default_error(&format!( + "{backend_name} requires HTTPS outside explicit development mode" + ))); + } + + if matches!(auth_method, VaultAuthMethod::Token { token } if token == "dev-token") { + return Err(development_default_error(&format!( + "{backend_name} default dev-token is not allowed outside explicit development mode" + ))); + } + + if tls.is_some_and(|tls| tls.skip_verify) { + return Err(development_default_error(&format!( + "{backend_name} skip TLS verification is not allowed outside explicit development mode" + ))); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -577,13 +665,14 @@ mod tests { fn test_default_config() { let config = KmsConfig::default(); assert_eq!(config.backend, KmsBackend::Local); - assert!(config.validate().is_ok()); + assert!(config.validate().is_err()); + assert!(config.with_insecure_development_defaults().validate().is_ok()); } #[test] fn test_local_config() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let config = KmsConfig::local(temp_dir.path().to_path_buf()); + let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults(); assert_eq!(config.backend, KmsBackend::Local); assert!(config.validate().is_ok()); @@ -592,6 +681,27 @@ mod tests { assert_eq!(local_config.key_dir, temp_dir.path()); } + #[test] + fn test_local_development_defaults_require_opt_in() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let config = KmsConfig::local(temp_dir.path().to_path_buf()); + + assert!(config.validate().is_err()); + assert!(config.with_insecure_development_defaults().validate().is_ok()); + + let production_config = KmsConfig { + backend: KmsBackend::Local, + backend_config: BackendConfig::Local(LocalConfig { + key_dir: PathBuf::from("/var/lib/rustfs/kms"), + master_key: Some("production-master-key".to_string()), + file_permissions: Some(0o600), + }), + ..Default::default() + }; + + assert!(production_config.validate().is_ok()); + } + #[test] fn test_vault_config() { let address = Url::parse("https://vault.example.com:8200").expect("Valid URL"); @@ -617,6 +727,42 @@ mod tests { assert_eq!(vault_config.mount_path, "transit"); } + #[test] + fn test_vault_development_defaults_require_opt_in() { + let http_address = Url::parse("http://127.0.0.1:8200").expect("Valid URL"); + let https_address = Url::parse("https://vault.example.com:8200").expect("Valid URL"); + + let http_config = KmsConfig::vault(http_address, "vault-token".to_string()); + assert!(http_config.validate().is_err()); + assert!(http_config.with_insecure_development_defaults().validate().is_ok()); + + let dev_token_config = KmsConfig::vault(https_address.clone(), "dev-token".to_string()); + assert!(dev_token_config.validate().is_err()); + assert!(dev_token_config.with_insecure_development_defaults().validate().is_ok()); + + let skip_tls_config = KmsConfig { + backend: KmsBackend::VaultTransit, + backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig { + address: https_address.to_string(), + auth_method: VaultAuthMethod::Token { + token: "vault-token".to_string(), + }, + namespace: None, + mount_path: "transit".to_string(), + tls: Some(TlsConfig { + ca_cert_path: None, + client_cert_path: None, + client_key_path: None, + skip_verify: true, + }), + })), + ..Default::default() + }; + + assert!(skip_tls_config.validate().is_err()); + assert!(skip_tls_config.with_insecure_development_defaults().validate().is_ok()); + } + #[test] fn test_vault_kv2_backend_serialization_uses_pascal_case() { let serialized = serde_json::to_string(&KmsBackend::VaultKv2).expect("backend should serialize"); @@ -710,7 +856,10 @@ mod tests { #[test] fn test_config_validation() { - let mut config = KmsConfig::default(); + let mut config = KmsConfig { + allow_insecure_dev_defaults: true, + ..Default::default() + }; // Valid config assert!(config.validate().is_ok()); @@ -760,6 +909,50 @@ mod tests { ); } + #[test] + fn test_from_env_requires_vault_development_opt_in() { + with_vars( + vec![ + ("RUSTFS_KMS_BACKEND", Some("vault")), + ("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")), + ("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")), + ], + || { + let error = KmsConfig::from_env().expect_err("vault dev defaults should fail closed"); + assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS)); + }, + ); + + with_vars( + vec![ + ("RUSTFS_KMS_BACKEND", Some("vault")), + ("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")), + ("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")), + (ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, Some("true")), + ], + || { + let config = KmsConfig::from_env().expect("explicit development opt-in should allow vault dev defaults"); + assert!(config.allow_insecure_dev_defaults); + }, + ); + } + + #[test] + fn test_from_env_rejects_vault_skip_tls_verify_without_opt_in() { + with_vars( + vec![ + ("RUSTFS_KMS_BACKEND", Some("vault-transit")), + ("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")), + ("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")), + (ENV_KMS_VAULT_SKIP_TLS_VERIFY, Some("true")), + ], + || { + let error = KmsConfig::from_env().expect_err("skip TLS verify should fail closed"); + assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS)); + }, + ); + } + #[test] fn test_from_env_reads_vault_transit_settings() { with_vars( diff --git a/crates/kms/src/lib.rs b/crates/kms/src/lib.rs index 3c968d07e..29cfd25ac 100644 --- a/crates/kms/src/lib.rs +++ b/crates/kms/src/lib.rs @@ -53,7 +53,7 @@ //! let service_manager = init_global_kms_service_manager(); //! //! // Configure with local backend -//! let config = KmsConfig::local(PathBuf::from("./kms_keys")); +//! let config = KmsConfig::local(PathBuf::from("./kms_keys")).with_insecure_development_defaults(); //! service_manager.configure(config).await?; //! //! // Start the KMS service @@ -135,7 +135,7 @@ mod tests { // Test configuration and start let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let config = KmsConfig::local(temp_dir.path().to_path_buf()); + let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults(); manager.configure(config).await.expect("Configuration should succeed"); manager.start().await.expect("Start should succeed"); @@ -160,7 +160,7 @@ mod tests { // Start first service let temp_dir1 = TempDir::new().expect("Failed to create temp dir"); - let config1 = KmsConfig::local(temp_dir1.path().to_path_buf()); + let config1 = KmsConfig::local(temp_dir1.path().to_path_buf()).with_insecure_development_defaults(); manager .configure(config1.clone()) .await @@ -176,7 +176,7 @@ mod tests { // Reconfigure to new service (zero-downtime) let temp_dir2 = TempDir::new().expect("Failed to create temp dir"); - let config2 = KmsConfig::local(temp_dir2.path().to_path_buf()); + let config2 = KmsConfig::local(temp_dir2.path().to_path_buf()).with_insecure_development_defaults(); manager.reconfigure(config2).await.expect("Reconfiguration should succeed"); // Verify version 2 @@ -205,7 +205,7 @@ mod tests { let base_path = temp_dir.path().to_path_buf(); // Initial configuration - let config1 = KmsConfig::local(base_path.clone()); + let config1 = KmsConfig::local(base_path.clone()).with_insecure_development_defaults(); manager.configure(config1).await.expect("Configuration should succeed"); manager.start().await.expect("Start should succeed"); @@ -215,7 +215,7 @@ mod tests { let manager_clone = manager.clone(); let path = base_path.clone(); let handle = tokio::spawn(async move { - let config = KmsConfig::local(path); + let config = KmsConfig::local(path).with_insecure_development_defaults(); manager_clone.reconfigure(config).await }); handles.push(handle); diff --git a/crates/kms/src/manager.rs b/crates/kms/src/manager.rs index fec6dfb49..1fb489e1a 100644 --- a/crates/kms/src/manager.rs +++ b/crates/kms/src/manager.rs @@ -166,7 +166,7 @@ mod tests { #[tokio::test] async fn test_manager_operations() { let temp_dir = tempdir().expect("Failed to create temp dir"); - let config = KmsConfig::local(temp_dir.path().to_path_buf()); + let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults(); let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend")); let manager = KmsManager::new(backend, config); @@ -216,7 +216,7 @@ mod tests { #[tokio::test] async fn generate_data_key_does_not_reuse_context_bound_ciphertext() { let temp_dir = tempdir().expect("Failed to create temp dir"); - let config = KmsConfig::local(temp_dir.path().to_path_buf()); + let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults(); let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend")); let manager = KmsManager::new(backend, config); diff --git a/crates/kms/src/service.rs b/crates/kms/src/service.rs index 03f893090..e2bd7e41b 100644 --- a/crates/kms/src/service.rs +++ b/crates/kms/src/service.rs @@ -738,7 +738,9 @@ mod tests { async fn create_test_service() -> (ObjectEncryptionService, TempDir) { let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_default_key("test-key".to_string()); + let config = KmsConfig::local(temp_dir.path().to_path_buf()) + .with_insecure_development_defaults() + .with_default_key("test-key".to_string()); let backend = Arc::new( crate::backends::local::LocalKmsBackend::new(config.clone()) .await diff --git a/crates/kms/src/service_manager.rs b/crates/kms/src/service_manager.rs index 7b850f2be..387cffa4e 100644 --- a/crates/kms/src/service_manager.rs +++ b/crates/kms/src/service_manager.rs @@ -92,6 +92,8 @@ impl KmsServiceManager { /// Configure KMS with new configuration pub async fn configure(&self, new_config: KmsConfig) -> Result<()> { + new_config.validate()?; + // Update configuration { let mut config = self.config.write().await; @@ -200,6 +202,7 @@ impl KmsServiceManager { let _guard = self.lifecycle_mutex.lock().await; info!("Reconfiguring KMS service (zero-downtime)"); + new_config.validate()?; // Configure with new config { @@ -307,6 +310,8 @@ impl KmsServiceManager { /// /// This creates a new backend, manager, and service, and assigns it a new version number. async fn create_service_version(&self, config: &KmsConfig) -> Result { + config.validate()?; + // Increment version counter let version = self.version_counter.fetch_add(1, Ordering::Relaxed) + 1; @@ -371,3 +376,22 @@ pub async fn get_global_encryption_service() -> Option, + /// Master key for local KMS key-file encryption + #[arg(long, env = "RUSTFS_KMS_LOCAL_MASTER_KEY")] + pub kms_local_master_key: Option, + /// Vault address for vault backend #[arg(long, env = "RUSTFS_KMS_VAULT_ADDRESS")] pub kms_vault_address: Option, @@ -270,6 +274,10 @@ pub struct ServerOpts { #[arg(long, env = "RUSTFS_KMS_DEFAULT_KEY_ID")] pub kms_default_key_id: Option, + /// Allow development-only insecure KMS defaults + #[arg(long, default_value_t = false, env = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS")] + pub kms_allow_insecure_dev_defaults: bool, + /// Disable adaptive buffer sizing with workload profiles /// Set this flag to use legacy fixed-size buffer behavior from PR #869 #[arg(long, default_value_t = false, env = "RUSTFS_BUFFER_PROFILE_DISABLE")] @@ -316,10 +324,12 @@ pub fn default_server_opts() -> ServerOpts { kms_enable: false, kms_backend: "local".to_string(), kms_key_dir: None, + kms_local_master_key: None, kms_vault_address: None, kms_vault_token: None, kms_vault_mount_path: None, kms_default_key_id: None, + kms_allow_insecure_dev_defaults: false, buffer_profile_disable: false, buffer_profile: "GeneralPurpose".to_string(), } diff --git a/rustfs/src/config/config_struct.rs b/rustfs/src/config/config_struct.rs index 1d6dc1e7e..b8971c2c9 100644 --- a/rustfs/src/config/config_struct.rs +++ b/rustfs/src/config/config_struct.rs @@ -125,6 +125,9 @@ pub struct Config { /// KMS key directory for local backend pub kms_key_dir: Option, + /// Master key for local KMS key-file encryption + pub kms_local_master_key: Option, + /// Vault address for vault backend pub kms_vault_address: Option, @@ -137,6 +140,9 @@ pub struct Config { /// Default KMS key ID for encryption pub kms_default_key_id: Option, + /// Allow development-only insecure KMS defaults + pub kms_allow_insecure_dev_defaults: bool, + /// Disable adaptive buffer sizing with workload profiles pub buffer_profile_disable: bool, @@ -165,10 +171,12 @@ impl Config { kms_enable: false, kms_backend: "local".to_string(), kms_key_dir: None, + kms_local_master_key: None, kms_vault_address: None, kms_vault_token: None, kms_vault_mount_path: None, kms_default_key_id: None, + kms_allow_insecure_dev_defaults: false, buffer_profile_disable: false, buffer_profile: "GeneralPurpose".to_string(), } @@ -197,10 +205,12 @@ impl Config { kms_enable, kms_backend, kms_key_dir, + kms_local_master_key, kms_vault_address, kms_vault_token, kms_vault_mount_path, kms_default_key_id, + kms_allow_insecure_dev_defaults, buffer_profile_disable, buffer_profile, } = opt; @@ -238,10 +248,12 @@ impl Config { kms_enable, kms_backend, kms_key_dir, + kms_local_master_key, kms_vault_address, kms_vault_token, kms_vault_mount_path, kms_default_key_id, + kms_allow_insecure_dev_defaults, buffer_profile_disable, buffer_profile, }) @@ -279,10 +291,12 @@ impl std::fmt::Debug for Config { .field("kms_enable", &self.kms_enable) .field("kms_backend", &self.kms_backend) .field("kms_key_dir", &self.kms_key_dir) + .field("kms_local_master_key", &Masked(self.kms_local_master_key.as_deref())) .field("kms_vault_address", &self.kms_vault_address) .field("kms_vault_token", &Masked(self.kms_vault_token.as_deref())) .field("kms_vault_mount_path", &self.kms_vault_mount_path) .field("kms_default_key_id", &self.kms_default_key_id) + .field("kms_allow_insecure_dev_defaults", &self.kms_allow_insecure_dev_defaults) .field("buffer_profile_disable", &self.buffer_profile_disable) .field("buffer_profile", &self.buffer_profile) .finish() diff --git a/rustfs/src/config/config_test.rs b/rustfs/src/config/config_test.rs index 57655f554..2db1bb430 100644 --- a/rustfs/src/config/config_test.rs +++ b/rustfs/src/config/config_test.rs @@ -133,10 +133,12 @@ mod tests { assert!(!config.kms_enable); assert_eq!(config.kms_backend, "local"); assert_eq!(config.kms_key_dir, None); + assert_eq!(config.kms_local_master_key, None); assert_eq!(config.kms_vault_address, None); assert_eq!(config.kms_vault_token, None); assert_eq!(config.kms_vault_mount_path, None); assert_eq!(config.kms_default_key_id, None); + assert!(!config.kms_allow_insecure_dev_defaults); assert!(!config.buffer_profile_disable); assert_eq!(config.buffer_profile, "GeneralPurpose"); } diff --git a/rustfs/src/config/opt.rs b/rustfs/src/config/opt.rs index ce973d921..5209ed5ff 100644 --- a/rustfs/src/config/opt.rs +++ b/rustfs/src/config/opt.rs @@ -44,10 +44,12 @@ pub struct Opt { pub kms_enable: bool, pub kms_backend: String, pub kms_key_dir: Option, + pub kms_local_master_key: Option, pub kms_vault_address: Option, pub kms_vault_token: Option, pub kms_vault_mount_path: Option, pub kms_default_key_id: Option, + pub kms_allow_insecure_dev_defaults: bool, pub buffer_profile_disable: bool, pub buffer_profile: String, } @@ -72,10 +74,12 @@ impl Opt { kms_enable: o.kms_enable, kms_backend: o.kms_backend, kms_key_dir: o.kms_key_dir, + kms_local_master_key: o.kms_local_master_key, kms_vault_address: o.kms_vault_address, kms_vault_token: o.kms_vault_token, kms_vault_mount_path: o.kms_vault_mount_path, kms_default_key_id: o.kms_default_key_id, + kms_allow_insecure_dev_defaults: o.kms_allow_insecure_dev_defaults, buffer_profile_disable: o.buffer_profile_disable, buffer_profile: o.buffer_profile, } diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index 8eedf02b2..520a1d682 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -174,9 +174,10 @@ fn build_local_kms_config(cfg: &config::Config) -> std::io::Result std::io::Result std::io::Result = ObjectTableCatalogStore TableCatalogObjectBackend for EcStoreTableCatalogObjectBackend where - S: StorageAPI, + S: StorageAPI + NamespaceLocking, { async fn read_object(&self, bucket: &str, object: &str) -> TableCatalogStoreResult> { self.read_object_with_options(bucket, object, ObjectOptions::default()).await