// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! KMS configuration management use crate::error::{KmsError, Result}; use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::time::Duration; use url::Url; /// KMS backend types #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum KmsBackend { /// Vault KV v2 + Transit backend (key metadata in KV, wrapping via Transit) #[serde(rename = "VaultKV2", alias = "Vault")] VaultKv2, /// Vault Transit backend using Vault as the cryptographic source of truth #[serde(rename = "VaultTransit")] VaultTransit, /// Local file-based backend for development and testing only #[default] Local, } /// Main KMS configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KmsConfig { /// Backend type pub backend: KmsBackend, /// Default master key ID for auto-encryption pub default_key_id: Option, /// Backend-specific configuration pub backend_config: BackendConfig, /// Operation timeout pub timeout: Duration, /// Number of retry attempts pub retry_attempts: u32, /// Enable caching pub enable_cache: bool, /// Cache configuration pub cache_config: CacheConfig, } impl Default for KmsConfig { fn default() -> Self { Self { backend: KmsBackend::default(), default_key_id: None, backend_config: BackendConfig::default(), timeout: Duration::from_secs(30), retry_attempts: 3, enable_cache: true, cache_config: CacheConfig::default(), } } } /// Backend-specific configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub enum BackendConfig { /// Local backend configuration Local(LocalConfig), /// Vault KV v2 + Transit backend configuration #[serde(rename = "VaultKV2", alias = "Vault")] VaultKv2(Box), /// Vault Transit backend configuration VaultTransit(Box), } impl Default for BackendConfig { fn default() -> Self { Self::Local(LocalConfig::default()) } } /// Local KMS backend configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocalConfig { /// Directory to store key files pub key_dir: PathBuf, /// Master key for encrypting stored keys (if None, keys are stored in plaintext) pub master_key: Option, /// File permissions for key files (octal) pub file_permissions: Option, } impl Default for LocalConfig { fn default() -> Self { Self { key_dir: std::env::temp_dir().join("rustfs_kms_keys"), master_key: None, file_permissions: Some(0o600), // Owner read/write only } } } /// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VaultConfig { /// Vault server URL pub address: String, /// Authentication method pub auth_method: VaultAuthMethod, /// Vault namespace (Vault Enterprise) pub namespace: Option, /// Transit engine mount path pub mount_path: String, /// KV engine mount path for storing keys pub kv_mount: String, /// Path prefix for keys in KV store pub key_path_prefix: String, /// TLS configuration pub tls: Option, } impl Default for VaultConfig { fn default() -> Self { Self { address: "http://localhost:8200".to_string(), auth_method: VaultAuthMethod::Token { token: "dev-token".to_string(), }, namespace: None, mount_path: "transit".to_string(), kv_mount: "secret".to_string(), key_path_prefix: "rustfs/kms/keys".to_string(), tls: None, } } } /// Vault Transit backend configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VaultTransitConfig { /// Vault server URL pub address: String, /// Authentication method pub auth_method: VaultAuthMethod, /// Vault namespace (Vault Enterprise) pub namespace: Option, /// Transit engine mount path pub mount_path: String, /// TLS configuration pub tls: Option, } impl Default for VaultTransitConfig { fn default() -> Self { Self { address: "http://localhost:8200".to_string(), auth_method: VaultAuthMethod::Token { token: "dev-token".to_string(), }, namespace: None, mount_path: "transit".to_string(), tls: None, } } } /// Vault authentication methods #[derive(Debug, Clone, Serialize, Deserialize)] pub enum VaultAuthMethod { /// Token authentication Token { token: String }, /// AppRole authentication AppRole { role_id: String, secret_id: String }, } /// TLS configuration for Vault #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsConfig { /// Path to CA certificate file pub ca_cert_path: Option, /// Path to client certificate file pub client_cert_path: Option, /// Path to client private key file pub client_key_path: Option, /// Skip TLS verification (insecure, for development only) pub skip_verify: bool, } /// Cache configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CacheConfig { /// Maximum number of keys to cache pub max_keys: usize, /// TTL for cached keys pub ttl: Duration, /// Enable cache metrics pub enable_metrics: bool, } impl Default for CacheConfig { fn default() -> Self { Self { max_keys: 1000, ttl: Duration::from_secs(3600), // 1 hour enable_metrics: true, } } } impl KmsConfig { /// Create a new KMS configuration for local backend (for development and testing only) pub fn local(key_dir: PathBuf) -> Self { Self { backend: KmsBackend::Local, backend_config: BackendConfig::Local(LocalConfig { key_dir, ..Default::default() }), ..Default::default() } } /// Create a new KMS configuration for Vault backend with token authentication (recommended for production) pub fn vault(address: Url, token: String) -> Self { Self { backend: KmsBackend::VaultKv2, backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::Token { token }, ..Default::default() })), ..Default::default() } } /// Create a new KMS configuration for Vault backend with AppRole authentication (recommended for production) pub fn vault_approle(address: Url, role_id: String, secret_id: String) -> Self { Self { backend: KmsBackend::VaultKv2, backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::AppRole { role_id, secret_id }, ..Default::default() })), ..Default::default() } } /// Create a new KMS configuration for Vault Transit backend with token authentication pub fn vault_transit(address: Url, token: String) -> Self { Self { backend: KmsBackend::VaultTransit, backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig { address: address.to_string(), auth_method: VaultAuthMethod::Token { token }, ..Default::default() })), ..Default::default() } } /// Get the local configuration if backend is Local pub fn local_config(&self) -> Option<&LocalConfig> { match &self.backend_config { BackendConfig::Local(config) => Some(config), _ => None, } } /// Get the Vault KV2 configuration if backend is VaultKv2 pub fn vault_config(&self) -> Option<&VaultConfig> { match &self.backend_config { BackendConfig::VaultKv2(config) => Some(config), _ => None, } } /// Get the Vault Transit configuration if backend is VaultTransit pub fn vault_transit_config(&self) -> Option<&VaultTransitConfig> { match &self.backend_config { BackendConfig::VaultTransit(config) => Some(config), _ => None, } } /// Set default key ID pub fn with_default_key(mut self, key_id: String) -> Self { self.default_key_id = Some(key_id); self } /// Set operation timeout pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } /// Enable or disable caching pub fn with_cache(mut self, enable: bool) -> Self { self.enable_cache = enable; self } /// Validate the configuration pub fn validate(&self) -> Result<()> { // Validate timeout if self.timeout.is_zero() { return Err(KmsError::configuration_error("Timeout must be greater than 0")); } // Validate retry attempts if self.retry_attempts == 0 { return Err(KmsError::configuration_error("Retry attempts must be greater than 0")); } // Validate backend-specific configuration match &self.backend_config { BackendConfig::Local(config) => { if !config.key_dir.is_absolute() { return Err(KmsError::configuration_error("Local key directory must be an absolute path")); } } 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 config.mount_path.is_empty() { return Err(KmsError::configuration_error("Vault KV2 mount path cannot be empty")); } // Validate TLS configuration if using HTTPS if config.address.starts_with("https://") && let Some(ref tls) = config.tls && !tls.skip_verify { // In production, we should have proper TLS configuration if tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() { tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA"); } } } BackendConfig::VaultTransit(config) => { if !config.address.starts_with("http://") && !config.address.starts_with("https://") { return Err(KmsError::configuration_error("Vault Transit address must use http or https scheme")); } if config.mount_path.is_empty() { return Err(KmsError::configuration_error("Vault Transit mount path cannot be empty")); } if config.address.starts_with("https://") && let Some(ref tls) = config.tls && !tls.skip_verify && tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() { tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA"); } } } // Validate cache configuration if self.enable_cache && self.cache_config.max_keys == 0 { return Err(KmsError::configuration_error("Cache max_keys must be greater than 0")); } Ok(()) } /// Load configuration from environment variables pub fn from_env() -> Result { let mut config = Self::default(); // Backend type if let Some(backend_type) = get_env_opt_str("RUSTFS_KMS_BACKEND") { config.backend = match backend_type.to_lowercase().as_str() { "local" => KmsBackend::Local, "vault" | "vault-kv2" | "vault_kv2" => KmsBackend::VaultKv2, "vault-transit" | "vault_transit" => KmsBackend::VaultTransit, _ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {backend_type}"))), }; } // Default key ID if let Some(key_id) = get_env_opt_str("RUSTFS_KMS_DEFAULT_KEY_ID") { config.default_key_id = Some(key_id); } // Timeout if let Some(timeout_str) = get_env_opt_str("RUSTFS_KMS_TIMEOUT_SECS") { let timeout_secs = timeout_str .parse::() .map_err(|_| KmsError::configuration_error("Invalid timeout value"))?; config.timeout = Duration::from_secs(timeout_secs); } // Retry attempts if let Some(retries_str) = get_env_opt_str("RUSTFS_KMS_RETRY_ATTEMPTS") { config.retry_attempts = retries_str .parse() .map_err(|_| KmsError::configuration_error("Invalid retry attempts value"))?; } // Enable cache config.enable_cache = get_env_bool("RUSTFS_KMS_ENABLE_CACHE", config.enable_cache); // Backend-specific configuration match config.backend { KmsBackend::Local => { let key_dir = get_env_str("RUSTFS_KMS_LOCAL_KEY_DIR", "./kms_keys"); let master_key = get_env_opt_str("RUSTFS_KMS_LOCAL_MASTER_KEY"); config.backend_config = BackendConfig::Local(LocalConfig { key_dir: PathBuf::from(key_dir), master_key, file_permissions: Some(0o600), }); } 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"); config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig { 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"), 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, })); } 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"); 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, })); } } config.validate()?; Ok(config) } } #[cfg(test)] mod tests { use super::*; use temp_env::with_vars; use tempfile::TempDir; #[test] fn test_default_config() { let config = KmsConfig::default(); assert_eq!(config.backend, KmsBackend::Local); assert!(config.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()); assert_eq!(config.backend, KmsBackend::Local); assert!(config.validate().is_ok()); let local_config = config.local_config().expect("Should have local config"); assert_eq!(local_config.key_dir, temp_dir.path()); } #[test] fn test_vault_config() { let address = Url::parse("https://vault.example.com:8200").expect("Valid URL"); let config = KmsConfig::vault(address.clone(), "test-token".to_string()); assert_eq!(config.backend, KmsBackend::VaultKv2); assert!(config.validate().is_ok()); let vault_config = config.vault_config().expect("Should have vault config"); assert_eq!(vault_config.address, address.as_str()); } #[test] fn test_vault_transit_config() { let address = Url::parse("https://vault.example.com:8200").expect("Valid URL"); let config = KmsConfig::vault_transit(address.clone(), "test-token".to_string()); assert_eq!(config.backend, KmsBackend::VaultTransit); assert!(config.validate().is_ok()); let vault_config = config.vault_transit_config().expect("Should have vault transit config"); assert_eq!(vault_config.address, address.as_str()); assert_eq!(vault_config.mount_path, "transit"); } #[test] fn test_vault_kv2_backend_serialization_uses_pascal_case() { let serialized = serde_json::to_string(&KmsBackend::VaultKv2).expect("backend should serialize"); assert_eq!(serialized, "\"VaultKV2\""); let legacy: KmsBackend = serde_json::from_str("\"Vault\"").expect("legacy Vault label should deserialize"); assert_eq!(legacy, KmsBackend::VaultKv2); } #[test] fn test_legacy_persisted_backend_config_vault_key_deserializes() { let raw = r#"{ "backend": "Vault", "backend_config": { "Vault": { "address": "http://127.0.0.1:8200", "auth_method": { "Token": { "token": "t" } }, "namespace": null, "mount_path": "transit", "kv_mount": "secret", "key_path_prefix": "rustfs/kms/keys", "tls": null } }, "default_key_id": null, "timeout": {"secs": 30, "nanos": 0}, "retry_attempts": 3, "enable_cache": true, "cache_config": { "max_keys": 1000, "ttl": {"secs": 3600, "nanos": 0}, "enable_metrics": true } }"#; let config: KmsConfig = serde_json::from_str(raw).expect("legacy persisted kms config"); assert_eq!(config.backend, KmsBackend::VaultKv2); assert!(config.vault_config().is_some()); } #[test] fn test_vault_transit_backend_serialization_uses_pascal_case() { let serialized = serde_json::to_string(&KmsBackend::VaultTransit).expect("backend should serialize"); assert_eq!(serialized, "\"VaultTransit\""); } #[test] fn test_config_validation() { let mut config = KmsConfig::default(); // Valid config assert!(config.validate().is_ok()); // Invalid timeout config.timeout = Duration::from_secs(0); assert!(config.validate().is_err()); // Reset timeout and test invalid retry attempts config.timeout = Duration::from_secs(30); config.retry_attempts = 0; assert!(config.validate().is_err()); } #[test] fn test_from_env_reads_vault_settings() { with_vars( vec![ ("RUSTFS_KMS_BACKEND", Some("vault")), ("RUSTFS_KMS_DEFAULT_KEY_ID", Some("tenant-key")), ("RUSTFS_KMS_TIMEOUT_SECS", Some("42")), ("RUSTFS_KMS_RETRY_ATTEMPTS", Some("7")), ("RUSTFS_KMS_ENABLE_CACHE", Some("false")), ("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")), ("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")), ("RUSTFS_KMS_VAULT_NAMESPACE", Some("tenant-a")), ("RUSTFS_KMS_VAULT_MOUNT_PATH", Some("transit-alt")), ("RUSTFS_KMS_VAULT_KV_MOUNT", Some("secret-alt")), ("RUSTFS_KMS_VAULT_KEY_PREFIX", Some("tenant/keys")), ], || { let config = KmsConfig::from_env().expect("kms config should load from env"); assert_eq!(config.backend, KmsBackend::VaultKv2); assert_eq!(config.default_key_id.as_deref(), Some("tenant-key")); assert_eq!(config.timeout, Duration::from_secs(42)); assert_eq!(config.retry_attempts, 7); assert!(!config.enable_cache); let vault = config.vault_config().expect("vault backend config"); assert_eq!(vault.address, "https://vault.example.com"); assert_eq!(vault.namespace.as_deref(), Some("tenant-a")); assert_eq!(vault.mount_path, "transit-alt"); assert_eq!(vault.kv_mount, "secret-alt"); assert_eq!(vault.key_path_prefix, "tenant/keys"); }, ); } #[test] fn test_from_env_reads_vault_transit_settings() { with_vars( vec![ ("RUSTFS_KMS_BACKEND", Some("vault-transit")), ("RUSTFS_KMS_DEFAULT_KEY_ID", Some("tenant-key")), ("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")), ("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")), ("RUSTFS_KMS_VAULT_NAMESPACE", Some("tenant-a")), ("RUSTFS_KMS_VAULT_MOUNT_PATH", Some("transit-alt")), ], || { let config = KmsConfig::from_env().expect("kms config should load from env"); assert_eq!(config.backend, KmsBackend::VaultTransit); assert_eq!(config.default_key_id.as_deref(), Some("tenant-key")); let vault = config.vault_transit_config().expect("vault transit backend config"); assert_eq!(vault.address, "https://vault.example.com"); assert_eq!(vault.namespace.as_deref(), Some("tenant-a")); assert_eq!(vault.mount_path, "transit-alt"); }, ); } }