From 30a757a9da99a0c75c0fef27ea73533316c5c15f Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 3 Apr 2026 00:46:00 +0800 Subject: [PATCH 1/4] feat(kms): add vault transit engine --- .vscode/launch.json | 20 +- crates/config/src/constants/app.rs | 3 + crates/e2e_test/src/kms/common.rs | 18 +- crates/e2e_test/src/kms/kms_vault_test.rs | 2 +- crates/kms/examples/kms_vault_kv_demo.rs | 2 +- crates/kms/src/api_types.rs | 235 +++++++- crates/kms/src/backends/local.rs | 4 +- crates/kms/src/backends/mod.rs | 1 + crates/kms/src/backends/vault.rs | 8 +- crates/kms/src/backends/vault_transit.rs | 635 ++++++++++++++++++++ crates/kms/src/config.rs | 208 ++++++- crates/kms/src/lib.rs | 8 +- crates/kms/src/service_manager.rs | 9 +- crates/kms/src/types.rs | 2 +- rustfs/src/admin/handlers/kms_dynamic.rs | 56 +- rustfs/src/admin/handlers/kms_management.rs | 42 +- rustfs/src/config/cli.rs | 7 +- rustfs/src/config/config_struct.rs | 6 + rustfs/src/config/opt.rs | 2 + rustfs/src/init.rs | 39 +- 20 files changed, 1232 insertions(+), 75 deletions(-) create mode 100644 crates/kms/src/backends/vault_transit.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c1483133..fb1187cbb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -197,19 +197,27 @@ // "__RUSTFS_SSE_SIMPLE_CMK": "2dfNXGHlsEflGVCxb+5DIdGEl1sIvtwX+QfmYasi5QM=", // 2. kms local backend test key - "RUSTFS_KMS_ENABLE": "true", - "RUSTFS_KMS_BACKEND": "local", - "RUSTFS_KMS_KEY_DIR": "./target/kms-key-dir", - "RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password - "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + // "RUSTFS_KMS_ENABLE": "true", + // "RUSTFS_KMS_BACKEND": "local", + // "RUSTFS_KMS_KEY_DIR": "./target/kms-key-dir", + // "RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password + // "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", // 3. kms vault backend test key // "RUSTFS_KMS_ENABLE": "true", - // "RUSTFS_KMS_BACKEND": "vault", + // "RUSTFS_KMS_BACKEND": "vault-kv2", // "RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200", // "RUSTFS_KMS_VAULT_TOKEN": "Dev Token", // "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + // 4. kms vault transit backend test key + "RUSTFS_KMS_ENABLE": "true", + "RUSTFS_KMS_BACKEND": "vault-transit", + "RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200", + "RUSTFS_KMS_VAULT_TOKEN": "Dev Token", + "RUSTFS_KMS_VAULT_MOUNT_PATH": "transit", + "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + }, "sourceLanguages": [ "rust" diff --git a/crates/config/src/constants/app.rs b/crates/config/src/constants/app.rs index f0dc5727b..0dae2eca6 100644 --- a/crates/config/src/constants/app.rs +++ b/crates/config/src/constants/app.rs @@ -172,6 +172,9 @@ pub const DEFAULT_KMS_ENABLE: bool = false; /// Environment variable for server KMS backend. pub const ENV_RUSTFS_KMS_BACKEND: &str = "RUSTFS_KMS_BACKEND"; +/// Environment variable for Vault Transit mount path. +pub const ENV_RUSTFS_KMS_VAULT_MOUNT_PATH: &str = "RUSTFS_KMS_VAULT_MOUNT_PATH"; + /// Default KMS backend for server-side encryption /// This is the default KMS backend for server-side encryption. /// Default value: local diff --git a/crates/e2e_test/src/kms/common.rs b/crates/e2e_test/src/kms/common.rs index 98c520358..9f824d51d 100644 --- a/crates/e2e_test/src/kms/common.rs +++ b/crates/e2e_test/src/kms/common.rs @@ -46,6 +46,7 @@ pub const VAULT_ADDRESS: &str = "127.0.0.1:8200"; pub const VAULT_TOKEN: &str = "dev-root-token"; pub const VAULT_TRANSIT_PATH: &str = "transit"; pub const VAULT_KEY_NAME: &str = "rustfs-master-key"; +pub const ENV_TEST_VAULT_BIN: &str = "RUSTFS_TEST_VAULT_BIN"; /// Initialize tracing for KMS tests with KMS-specific log levels pub fn init_logging() { @@ -385,6 +386,10 @@ pub struct VaultTestEnvironment { } impl VaultTestEnvironment { + fn resolve_vault_binary() -> String { + std::env::var(ENV_TEST_VAULT_BIN).unwrap_or_else(|_| "vault".to_string()) + } + /// Create a new Vault test environment pub async fn new() -> Result> { let base_env = RustFSTestEnvironment::new().await?; @@ -399,7 +404,8 @@ impl VaultTestEnvironment { pub async fn start_vault(&mut self) -> Result<(), Box> { info!("Starting Vault server in development mode"); - let vault_process = Command::new("vault") + let vault_binary = Self::resolve_vault_binary(); + let vault_process = Command::new(&vault_binary) .args([ "server", "-dev", @@ -490,10 +496,10 @@ impl VaultTestEnvironment { self.base_env.start_rustfs_server(Vec::new()).await } - /// Configure Vault KMS backend - pub async fn configure_vault_kms(&self) -> Result<(), Box> { + /// Configure Vault Transit KMS backend + pub async fn configure_vault_transit_kms(&self) -> Result<(), Box> { let kms_config = serde_json::json!({ - "backend_type": "vault", + "backend_type": "VaultTransit", "address": VAULT_URL, "auth_method": { "Token": { @@ -501,8 +507,6 @@ impl VaultTestEnvironment { } }, "mount_path": VAULT_TRANSIT_PATH, - "kv_mount": "secret", - "key_path_prefix": "rustfs/kms/keys", "default_key_id": VAULT_KEY_NAME, "skip_tls_verify": true }) @@ -787,7 +791,7 @@ impl LocalKMSTestEnvironment { // Configure KMS with the default key in one step let kms_config = serde_json::json!({ - "backend_type": "local", + "backend_type": "Local", "key_dir": self.kms_keys_dir, "file_permissions": 0o600, "default_key_id": default_key_id diff --git a/crates/e2e_test/src/kms/kms_vault_test.rs b/crates/e2e_test/src/kms/kms_vault_test.rs index 2377afb8e..0a1201bd8 100644 --- a/crates/e2e_test/src/kms/kms_vault_test.rs +++ b/crates/e2e_test/src/kms/kms_vault_test.rs @@ -42,7 +42,7 @@ impl VaultKmsTestContext { env.setup_vault_transit().await?; env.start_rustfs_for_vault().await?; - env.configure_vault_kms().await?; + env.configure_vault_transit_kms().await?; start_kms(&env.base_env.url, &env.base_env.access_key, &env.base_env.secret_key).await?; diff --git a/crates/kms/examples/kms_vault_kv_demo.rs b/crates/kms/examples/kms_vault_kv_demo.rs index 0d0200baa..9b518bfad 100644 --- a/crates/kms/examples/kms_vault_kv_demo.rs +++ b/crates/kms/examples/kms_vault_kv_demo.rs @@ -114,7 +114,7 @@ async fn main() -> Result<(), Box> { let mut tags = HashMap::new(); tags.insert("environment".to_string(), "demo".to_string()); tags.insert("purpose".to_string(), "testing".to_string()); - tags.insert("backend".to_string(), "vault".to_string()); + tags.insert("backend".to_string(), "vault-kv2".to_string()); tags }, origin: Some("demo2.rs".to_string()), diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index 087d26083..7a7bbe343 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -14,7 +14,9 @@ //! API types for KMS dynamic configuration -use crate::config::{BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig}; +use crate::config::{ + BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig, +}; use crate::service_manager::KmsServiceStatus; use crate::types::{KeyMetadata, KeyUsage}; use serde::{Deserialize, Serialize}; @@ -45,7 +47,7 @@ pub struct ConfigureLocalKmsRequest { pub cache_ttl_seconds: Option, } -/// Request to configure KMS with Vault backend +/// Request to configure KMS with Vault KV v2 + Transit backend #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigureVaultKmsRequest { /// Vault server URL @@ -76,14 +78,50 @@ pub struct ConfigureVaultKmsRequest { pub cache_ttl_seconds: Option, } +/// Request to configure KMS with Vault Transit backend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigureVaultTransitKmsRequest { + /// Vault server URL + pub address: String, + /// Authentication method + pub auth_method: VaultAuthMethod, + /// Vault namespace (Vault Enterprise, optional) + pub namespace: Option, + /// Transit engine mount path + pub mount_path: Option, + /// Skip TLS verification (insecure, for development only) + pub skip_tls_verify: Option, + /// Default master key ID for auto-encryption + pub default_key_id: Option, + /// Operation timeout in seconds + pub timeout_seconds: Option, + /// Number of retry attempts + pub retry_attempts: Option, + /// Enable caching + pub enable_cache: Option, + /// Maximum number of keys to cache + pub max_cached_keys: Option, + /// Cache TTL in seconds + pub cache_ttl_seconds: Option, +} + /// Generic KMS configuration request #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "backend_type", rename_all = "lowercase")] +#[serde(tag = "backend_type")] pub enum ConfigureKmsRequest { /// Configure with Local backend Local(ConfigureLocalKmsRequest), - /// Configure with Vault backend - Vault(ConfigureVaultKmsRequest), + /// Configure with Vault KV v2 + Transit backend + #[serde( + rename = "VaultKV2", + alias = "Vault", + alias = "vault", + alias = "vault-kv2", + alias = "vault_kv2" + )] + VaultKv2(ConfigureVaultKmsRequest), + /// Configure with Vault Transit backend + VaultTransit(ConfigureVaultTransitKmsRequest), } /// KMS configuration response @@ -152,6 +190,10 @@ pub struct KmsConfigSummary { pub retry_attempts: u32, /// Whether caching is enabled pub enable_cache: bool, + /// Maximum number of cached keys + pub max_cached_keys: usize, + /// Cache TTL in seconds + pub cache_ttl_seconds: u64, /// Cache configuration summary pub cache_summary: Option, /// Backend-specific summary @@ -171,7 +213,7 @@ pub struct CacheSummary { /// Backend-specific configuration summary #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "backend_type", rename_all = "lowercase")] +#[serde(tag = "backend_type", rename_all = "kebab-case")] pub enum BackendSummary { /// Local backend summary Local { @@ -182,12 +224,15 @@ pub enum BackendSummary { /// File permissions (octal) file_permissions: Option, }, - /// Vault backend summary - Vault { + /// Vault KV v2 + Transit backend summary + #[serde(alias = "vault")] + VaultKv2 { /// Vault server address address: String, /// Authentication method type auth_method_type: String, + /// Whether backend credentials are configured + has_stored_credentials: bool, /// Namespace (if configured) namespace: Option, /// Transit engine mount path @@ -196,6 +241,23 @@ pub enum BackendSummary { kv_mount: String, /// Key path prefix key_path_prefix: String, + /// Skip TLS verification + skip_tls_verify: bool, + }, + /// Vault Transit backend summary + VaultTransit { + /// Vault server address + address: String, + /// Authentication method type + auth_method_type: String, + /// Whether backend credentials are configured + has_stored_credentials: bool, + /// Namespace (if configured) + namespace: Option, + /// Transit engine mount path + mount_path: String, + /// Skip TLS verification + skip_tls_verify: bool, }, } @@ -217,16 +279,29 @@ impl From<&KmsConfig> for KmsConfigSummary { has_master_key: local_config.master_key.is_some(), file_permissions: local_config.file_permissions, }, - BackendConfig::Vault(vault_config) => BackendSummary::Vault { + BackendConfig::VaultKv2(vault_config) => BackendSummary::VaultKv2 { address: vault_config.address.clone(), auth_method_type: match &vault_config.auth_method { VaultAuthMethod::Token { .. } => "token".to_string(), VaultAuthMethod::AppRole { .. } => "approle".to_string(), }, + has_stored_credentials: true, namespace: vault_config.namespace.clone(), mount_path: vault_config.mount_path.clone(), kv_mount: vault_config.kv_mount.clone(), key_path_prefix: vault_config.key_path_prefix.clone(), + skip_tls_verify: vault_config.tls.as_ref().is_some_and(|tls| tls.skip_verify), + }, + BackendConfig::VaultTransit(vault_config) => BackendSummary::VaultTransit { + address: vault_config.address.clone(), + auth_method_type: match &vault_config.auth_method { + VaultAuthMethod::Token { .. } => "token".to_string(), + VaultAuthMethod::AppRole { .. } => "approle".to_string(), + }, + has_stored_credentials: true, + namespace: vault_config.namespace.clone(), + mount_path: vault_config.mount_path.clone(), + skip_tls_verify: vault_config.tls.as_ref().is_some_and(|tls| tls.skip_verify), }, }; @@ -236,6 +311,8 @@ impl From<&KmsConfig> for KmsConfigSummary { timeout_seconds: config.timeout.as_secs(), retry_attempts: config.retry_attempts, enable_cache: config.enable_cache, + max_cached_keys: config.cache_config.max_keys, + cache_ttl_seconds: config.cache_config.ttl.as_secs(), cache_summary, backend_summary, } @@ -269,9 +346,9 @@ impl ConfigureVaultKmsRequest { /// Convert to KmsConfig pub fn to_kms_config(&self) -> KmsConfig { KmsConfig { - backend: KmsBackend::Vault, + backend: KmsBackend::VaultKv2, default_key_id: self.default_key_id.clone(), - backend_config: BackendConfig::Vault(Box::new(VaultConfig { + backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig { address: self.address.clone(), auth_method: self.auth_method.clone(), namespace: self.namespace.clone(), @@ -301,12 +378,146 @@ impl ConfigureVaultKmsRequest { } } +impl ConfigureVaultTransitKmsRequest { + /// Convert to KmsConfig + pub fn to_kms_config(&self) -> KmsConfig { + KmsConfig { + backend: KmsBackend::VaultTransit, + default_key_id: self.default_key_id.clone(), + backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig { + address: self.address.clone(), + auth_method: self.auth_method.clone(), + namespace: self.namespace.clone(), + mount_path: self.mount_path.clone().unwrap_or_else(|| "transit".to_string()), + tls: if self.skip_tls_verify.unwrap_or(false) { + Some(TlsConfig { + ca_cert_path: None, + client_cert_path: None, + client_key_path: None, + skip_verify: true, + }) + } else { + None + }, + })), + 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), + cache_config: CacheConfig { + max_keys: self.max_cached_keys.unwrap_or(1000), + ttl: Duration::from_secs(self.cache_ttl_seconds.unwrap_or(3600)), + enable_metrics: true, + }, + } + } +} + impl ConfigureKmsRequest { /// Convert to KmsConfig pub fn to_kms_config(&self) -> KmsConfig { match self { ConfigureKmsRequest::Local(req) => req.to_kms_config(), - ConfigureKmsRequest::Vault(req) => req.to_kms_config(), + ConfigureKmsRequest::VaultKv2(req) => req.to_kms_config(), + ConfigureKmsRequest::VaultTransit(req) => req.to_kms_config(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() { + let bases = ["VaultKV2", "Vault", "vault", "vault-kv2", "vault_kv2"]; + for backend_type in bases { + let raw = serde_json::json!({ + "backend_type": backend_type, + "address": "http://127.0.0.1:8200", + "auth_method": { + "Token": { + "token": "dev-root-token" + } + }, + "mount_path": "transit", + "default_key_id": "rustfs-master-key" + }); + + let request: ConfigureKmsRequest = serde_json::from_value(raw).unwrap_or_else(|e| panic!("{backend_type}: {e}")); + let config = request.to_kms_config(); + assert_eq!(config.backend, KmsBackend::VaultKv2, "backend_type={backend_type}"); + let vault = config.vault_config().expect("vault-kv2 config"); + assert_eq!(vault.mount_path, "transit"); + } + } + + #[test] + fn test_deserialize_vault_transit_configure_request() { + let raw = serde_json::json!({ + "backend_type": "VaultTransit", + "address": "http://127.0.0.1:8200", + "auth_method": { + "Token": { + "token": "dev-root-token" + } + }, + "mount_path": "transit", + "default_key_id": "rustfs-master-key" + }); + + let request: ConfigureKmsRequest = serde_json::from_value(raw).expect("vault-transit request should deserialize"); + let config = request.to_kms_config(); + + assert_eq!(config.backend, KmsBackend::VaultTransit); + let vault = config.vault_transit_config().expect("vault-transit config should be present"); + assert_eq!(vault.mount_path, "transit"); + } + + #[test] + fn test_vault_transit_summary_reports_backend_details() { + let config = KmsConfig { + backend: KmsBackend::VaultTransit, + default_key_id: Some("rustfs-master-key".to_string()), + backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig { + address: "http://127.0.0.1:8200".to_string(), + auth_method: VaultAuthMethod::Token { + token: "dev-root-token".to_string(), + }, + namespace: Some("tenant-a".to_string()), + mount_path: "transit".to_string(), + tls: None, + })), + timeout: Duration::from_secs(30), + retry_attempts: 3, + enable_cache: true, + cache_config: CacheConfig::default(), + }; + + let summary = KmsConfigSummary::from(&config); + assert_eq!(summary.backend_type, KmsBackend::VaultTransit); + assert_eq!(summary.timeout_seconds, 30); + assert_eq!(summary.retry_attempts, 3); + assert_eq!(summary.max_cached_keys, 1000); + assert_eq!(summary.cache_ttl_seconds, 3600); + + match summary.backend_summary { + BackendSummary::VaultTransit { + address, + auth_method_type, + has_stored_credentials, + namespace, + mount_path, + skip_tls_verify, + .. + } => { + assert_eq!(address, "http://127.0.0.1:8200"); + assert_eq!(auth_method_type, "token"); + assert!(has_stored_credentials); + assert_eq!(namespace.as_deref(), Some("tenant-a")); + assert_eq!(mount_path, "transit"); + assert!(!skip_tls_verify); + } + other => panic!("expected vault-transit summary, got {other:?}"), } } } diff --git a/crates/kms/src/backends/local.rs b/crates/kms/src/backends/local.rs index 6ed9e7aa0..348011add 100644 --- a/crates/kms/src/backends/local.rs +++ b/crates/kms/src/backends/local.rs @@ -566,7 +566,9 @@ impl LocalKmsBackend { pub async fn new(config: KmsConfig) -> Result { let local_config = match &config.backend_config { crate::config::BackendConfig::Local(local_config) => local_config.clone(), - _ => return Err(KmsError::configuration_error("Expected Local backend configuration")), + crate::config::BackendConfig::VaultKv2(_) | crate::config::BackendConfig::VaultTransit(_) => { + return Err(KmsError::configuration_error("Expected Local backend configuration")); + } }; let client = LocalKmsClient::new(local_config).await?; diff --git a/crates/kms/src/backends/mod.rs b/crates/kms/src/backends/mod.rs index 1add6fe63..6c5eacfab 100644 --- a/crates/kms/src/backends/mod.rs +++ b/crates/kms/src/backends/mod.rs @@ -21,6 +21,7 @@ use std::collections::HashMap; pub mod local; pub mod vault; +pub mod vault_transit; /// Abstract KMS client interface that all backends must implement #[async_trait] diff --git a/crates/kms/src/backends/vault.rs b/crates/kms/src/backends/vault.rs index 971eb2de3..ae32ea6ba 100644 --- a/crates/kms/src/backends/vault.rs +++ b/crates/kms/src/backends/vault.rs @@ -581,7 +581,7 @@ impl KmsClient for VaultKmsClient { } fn backend_info(&self) -> BackendInfo { - BackendInfo::new("vault".to_string(), "0.1.0".to_string(), self.config.address.clone(), true) + BackendInfo::new("vault-kv2".to_string(), "0.1.0".to_string(), self.config.address.clone(), true) .with_metadata("kv_mount".to_string(), self.kv_mount.clone()) .with_metadata("key_prefix".to_string(), self.key_path_prefix.clone()) } @@ -596,8 +596,10 @@ impl VaultKmsBackend { /// Create a new VaultKmsBackend pub async fn new(config: KmsConfig) -> Result { let vault_config = match &config.backend_config { - crate::config::BackendConfig::Vault(vault_config) => (**vault_config).clone(), - _ => return Err(KmsError::configuration_error("Expected Vault backend configuration")), + crate::config::BackendConfig::VaultKv2(vault_config) => (**vault_config).clone(), + crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::VaultTransit(_) => { + return Err(KmsError::configuration_error("Expected Vault KV2 backend configuration")); + } }; let client = VaultKmsClient::new(vault_config).await?; diff --git a/crates/kms/src/backends/vault_transit.rs b/crates/kms/src/backends/vault_transit.rs new file mode 100644 index 000000000..08a3e03e7 --- /dev/null +++ b/crates/kms/src/backends/vault_transit.rs @@ -0,0 +1,635 @@ +// 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. + +//! Vault Transit-based KMS backend. + +use crate::backends::{BackendInfo, KmsBackend, KmsClient}; +use crate::config::{KmsConfig, VaultTransitConfig}; +use crate::encryption::{DataKeyEnvelope, generate_key_material}; +use crate::error::{KmsError, Result}; +use crate::types::*; +use async_trait::async_trait; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use jiff::Zoned; +use std::collections::{BTreeMap, HashMap}; +use std::time::Duration; +use tokio::sync::RwLock; +use vaultrs::{ + api::transit::{ + KeyType, + requests::{CreateKeyRequestBuilder, DecryptDataRequestBuilder, EncryptDataRequestBuilder}, + }, + client::{VaultClient, VaultClientSettingsBuilder}, + transit::{data, key}, +}; + +#[derive(Debug, Clone)] +struct TransitKeyMetadata { + key_usage: KeyUsage, + description: Option, + tags: HashMap, + key_state: KeyState, + created_at: Zoned, + deletion_date: Option, + origin: String, + created_by: Option, + current_version: u32, +} + +impl TransitKeyMetadata { + fn from_create_request(request: &CreateKeyRequest) -> Self { + Self { + key_usage: request.key_usage.clone(), + description: request.description.clone(), + tags: request.tags.clone(), + key_state: KeyState::Enabled, + created_at: Zoned::now(), + deletion_date: None, + origin: request.origin.clone().unwrap_or_else(|| "VAULT_TRANSIT".to_string()), + created_by: None, + current_version: 1, + } + } + + fn synthesized() -> Self { + Self { + key_usage: KeyUsage::EncryptDecrypt, + description: None, + tags: HashMap::new(), + key_state: KeyState::Enabled, + created_at: Zoned::now(), + deletion_date: None, + origin: "VAULT_TRANSIT".to_string(), + created_by: None, + current_version: 1, + } + } +} + +pub struct VaultTransitKmsClient { + client: VaultClient, + config: VaultTransitConfig, + metadata_cache: RwLock>, +} + +impl VaultTransitKmsClient { + pub async fn new(config: VaultTransitConfig) -> Result { + let mut settings_builder = VaultClientSettingsBuilder::default(); + settings_builder.address(&config.address); + + let token = match &config.auth_method { + crate::config::VaultAuthMethod::Token { token } => token.clone(), + crate::config::VaultAuthMethod::AppRole { .. } => { + return Err(KmsError::backend_error( + "AppRole authentication not yet implemented. Please use token authentication.", + )); + } + }; + + settings_builder.token(&token); + + if let Some(namespace) = &config.namespace { + settings_builder.namespace(Some(namespace.clone())); + } + + let settings = settings_builder + .build() + .map_err(|e| KmsError::backend_error(format!("Failed to build Vault client settings: {e}")))?; + + let client = + VaultClient::new(settings).map_err(|e| KmsError::backend_error(format!("Failed to create Vault client: {e}")))?; + + Ok(Self { + client, + config, + metadata_cache: RwLock::new(HashMap::new()), + }) + } + + fn canonicalize_context(encryption_context: &HashMap) -> Result> { + if encryption_context.is_empty() { + return Ok(None); + } + + let ordered: BTreeMap<_, _> = encryption_context + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + let serialized = serde_json::to_vec(&ordered)?; + Ok(Some(BASE64.encode(serialized))) + } + + fn map_vault_error(key_id: &str, error: vaultrs::error::ClientError, operation: &str) -> Result { + match error { + vaultrs::error::ClientError::ResponseWrapError => Err(KmsError::key_not_found(key_id)), + vaultrs::error::ClientError::APIError { code: 404, .. } => Err(KmsError::key_not_found(key_id)), + other => Err(KmsError::backend_error(format!( + "Vault Transit {operation} failed for key {key_id}: {other}" + ))), + } + } + + async fn read_transit_key(&self, key_id: &str) -> Result { + key::read(&self.client, &self.config.mount_path, key_id) + .await + .or_else(|e| Self::map_vault_error(key_id, e, "read")) + } + + async fn create_transit_key(&self, key_id: &str) -> Result<()> { + let mut builder = CreateKeyRequestBuilder::default(); + builder.key_type(KeyType::Aes256Gcm96); + key::create(&self.client, &self.config.mount_path, key_id, Some(&mut builder)) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to create Vault Transit key {key_id}: {e}"))) + } + + async fn transit_encrypt( + &self, + key_id: &str, + plaintext: &[u8], + encryption_context: &HashMap, + ) -> Result { + let plaintext_b64 = BASE64.encode(plaintext); + let mut builder = EncryptDataRequestBuilder::default(); + if let Some(aad) = Self::canonicalize_context(encryption_context)? { + builder.associated_data(aad); + } + + let response = data::encrypt(&self.client, &self.config.mount_path, key_id, &plaintext_b64, Some(&mut builder)) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to encrypt data with Vault Transit key {key_id}: {e}")))?; + + Ok(response.ciphertext) + } + + async fn transit_decrypt( + &self, + key_id: &str, + ciphertext: &str, + encryption_context: &HashMap, + ) -> Result> { + let mut builder = DecryptDataRequestBuilder::default(); + if let Some(aad) = Self::canonicalize_context(encryption_context)? { + builder.associated_data(aad); + } + + let response = data::decrypt(&self.client, &self.config.mount_path, key_id, ciphertext, Some(&mut builder)) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to decrypt data with Vault Transit key {key_id}: {e}")))?; + + BASE64 + .decode(response.plaintext) + .map_err(|e| KmsError::cryptographic_error("base64_decode", e.to_string())) + } + + async fn get_key_metadata(&self, key_id: &str) -> Result { + if let Some(metadata) = self.metadata_cache.read().await.get(key_id).cloned() { + return Ok(metadata); + } + + self.read_transit_key(key_id).await?; + let metadata = TransitKeyMetadata::synthesized(); + self.metadata_cache.write().await.insert(key_id.to_string(), metadata.clone()); + Ok(metadata) + } + + async fn store_key_metadata(&self, key_id: &str, metadata: &TransitKeyMetadata) -> Result<()> { + self.metadata_cache.write().await.insert(key_id.to_string(), metadata.clone()); + Ok(()) + } + + async fn delete_key_metadata(&self, key_id: &str) -> Result<()> { + self.metadata_cache.write().await.remove(key_id); + Ok(()) + } + + async fn key_info(&self, key_id: &str) -> Result { + self.read_transit_key(key_id).await?; + let metadata = self.get_key_metadata(key_id).await?; + + Ok(KeyInfo { + key_id: key_id.to_string(), + description: metadata.description.clone(), + algorithm: "AES_256".to_string(), + usage: metadata.key_usage.clone(), + status: match metadata.key_state { + KeyState::Enabled => KeyStatus::Active, + KeyState::Disabled => KeyStatus::Disabled, + KeyState::PendingDeletion => KeyStatus::PendingDeletion, + KeyState::PendingImport | KeyState::Unavailable => KeyStatus::Deleted, + }, + version: metadata.current_version, + metadata: metadata.tags.clone(), + tags: metadata.tags, + created_at: metadata.created_at, + rotated_at: None, + created_by: metadata.created_by, + }) + } + + async fn key_metadata_response(&self, key_id: &str) -> Result { + self.read_transit_key(key_id).await?; + let metadata = self.get_key_metadata(key_id).await?; + + Ok(KeyMetadata { + key_id: key_id.to_string(), + key_state: metadata.key_state, + key_usage: metadata.key_usage, + description: metadata.description, + creation_date: metadata.created_at, + deletion_date: metadata.deletion_date, + origin: metadata.origin, + key_manager: "VAULT_TRANSIT".to_string(), + tags: metadata.tags, + }) + } + + async fn ensure_key_active(&self, key_id: &str) -> Result { + let metadata = self.get_key_metadata(key_id).await?; + if metadata.key_state != KeyState::Enabled { + return Err(KmsError::invalid_operation(format!( + "Key {key_id} is not active (state: {:?})", + metadata.key_state + ))); + } + Ok(metadata) + } +} + +#[async_trait] +impl KmsClient for VaultTransitKmsClient { + async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result { + self.ensure_key_active(&request.master_key_id).await?; + + let plaintext_key = generate_key_material(&request.key_spec)?; + let encrypted_key = self + .transit_encrypt(&request.master_key_id, &plaintext_key, &request.encryption_context) + .await?; + + let envelope = DataKeyEnvelope { + key_id: uuid::Uuid::new_v4().to_string(), + master_key_id: request.master_key_id.clone(), + key_spec: request.key_spec.clone(), + encrypted_key: encrypted_key.into_bytes(), + nonce: Vec::new(), + encryption_context: request.encryption_context.clone(), + created_at: Zoned::now(), + }; + + let ciphertext = serde_json::to_vec(&envelope)?; + Ok(DataKeyInfo::new( + envelope.key_id, + 1, + Some(plaintext_key), + ciphertext, + request.key_spec.clone(), + )) + } + + async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result { + let metadata = self.ensure_key_active(&request.key_id).await?; + let ciphertext = self + .transit_encrypt(&request.key_id, &request.plaintext, &request.encryption_context) + .await?; + + Ok(EncryptResponse { + ciphertext: ciphertext.into_bytes(), + key_id: request.key_id.clone(), + key_version: metadata.current_version, + algorithm: "vault-transit".to_string(), + }) + } + + async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result> { + let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) + .map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?; + + for (key, expected_value) in &envelope.encryption_context { + if let Some(actual_value) = request.encryption_context.get(key) { + if actual_value != expected_value { + return Err(KmsError::context_mismatch(format!( + "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" + ))); + } + } else if !request.encryption_context.is_empty() { + return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))); + } + } + + let encrypted_key = std::str::from_utf8(&envelope.encrypted_key) + .map_err(|e| KmsError::cryptographic_error("utf8", format!("Invalid Transit ciphertext: {e}")))?; + self.transit_decrypt(&envelope.master_key_id, encrypted_key, &envelope.encryption_context) + .await + } + + async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result { + if algorithm != "AES_256" { + return Err(KmsError::unsupported_algorithm(algorithm)); + } + + if self.read_transit_key(key_id).await.is_ok() { + return Err(KmsError::key_already_exists(key_id)); + } + + self.create_transit_key(key_id).await?; + + let metadata = TransitKeyMetadata { + created_by: Some("vault-transit".to_string()), + ..TransitKeyMetadata::from_create_request(&CreateKeyRequest { + key_name: Some(key_id.to_string()), + ..Default::default() + }) + }; + self.store_key_metadata(key_id, &metadata).await?; + + Ok(MasterKeyInfo { + key_id: key_id.to_string(), + version: metadata.current_version, + algorithm: algorithm.to_string(), + usage: metadata.key_usage, + status: KeyStatus::Active, + description: metadata.description, + metadata: metadata.tags, + created_at: metadata.created_at, + rotated_at: None, + created_by: metadata.created_by, + }) + } + + async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { + self.key_info(key_id).await + } + + async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result { + let all_keys = key::list(&self.client, &self.config.mount_path) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to list Vault Transit keys: {e}")))? + .keys; + + let mut filtered = Vec::new(); + for key_id in all_keys { + let key_info = self.key_info(&key_id).await?; + let usage_matches = request.usage_filter.as_ref().is_none_or(|usage| usage == &key_info.usage); + let status_matches = request.status_filter.as_ref().is_none_or(|status| status == &key_info.status); + if usage_matches && status_matches { + filtered.push(key_info); + } + } + + let start_idx = request + .marker + .as_ref() + .and_then(|marker| filtered.iter().position(|info| &info.key_id == marker)) + .map(|idx| idx + 1) + .unwrap_or(0); + let limit = request.limit.unwrap_or(100) as usize; + let end_idx = std::cmp::min(start_idx + limit, filtered.len()); + let keys = filtered[start_idx..end_idx].to_vec(); + let next_marker = if end_idx < filtered.len() { + Some(filtered[end_idx - 1].key_id.clone()) + } else { + None + }; + + Ok(ListKeysResponse { + keys, + next_marker, + truncated: end_idx < filtered.len(), + }) + } + + async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { + let mut metadata = self.get_key_metadata(key_id).await?; + metadata.key_state = KeyState::Enabled; + metadata.deletion_date = None; + self.store_key_metadata(key_id, &metadata).await + } + + async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { + let mut metadata = self.get_key_metadata(key_id).await?; + metadata.key_state = KeyState::Disabled; + self.store_key_metadata(key_id, &metadata).await + } + + async fn schedule_key_deletion( + &self, + key_id: &str, + pending_window_days: u32, + _context: Option<&OperationContext>, + ) -> Result<()> { + let mut metadata = self.get_key_metadata(key_id).await?; + metadata.key_state = KeyState::PendingDeletion; + metadata.deletion_date = Some(Zoned::now() + Duration::from_secs(pending_window_days as u64 * 86400)); + self.store_key_metadata(key_id, &metadata).await + } + + async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { + let mut metadata = self.get_key_metadata(key_id).await?; + metadata.key_state = KeyState::Enabled; + metadata.deletion_date = None; + self.store_key_metadata(key_id, &metadata).await + } + + async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { + key::rotate(&self.client, &self.config.mount_path, key_id) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to rotate Vault Transit key {key_id}: {e}")))?; + + let mut metadata = self.get_key_metadata(key_id).await?; + metadata.current_version += 1; + self.store_key_metadata(key_id, &metadata).await?; + + Ok(MasterKeyInfo { + key_id: key_id.to_string(), + version: metadata.current_version, + algorithm: "AES_256".to_string(), + usage: metadata.key_usage, + status: KeyStatus::Active, + description: metadata.description, + metadata: metadata.tags, + created_at: metadata.created_at, + rotated_at: Some(Zoned::now()), + created_by: metadata.created_by, + }) + } + + async fn health_check(&self) -> Result<()> { + key::list(&self.client, &self.config.mount_path) + .await + .map(|_| ()) + .map_err(|e| KmsError::backend_error(format!("Vault Transit health check failed: {e}"))) + } + + fn backend_info(&self) -> BackendInfo { + BackendInfo::new("vault-transit".to_string(), "0.1.0".to_string(), self.config.address.clone(), true) + .with_metadata("mount_path".to_string(), self.config.mount_path.clone()) + } +} + +pub struct VaultTransitKmsBackend { + client: VaultTransitKmsClient, +} + +impl VaultTransitKmsBackend { + pub async fn new(config: KmsConfig) -> Result { + let vault_config = match &config.backend_config { + crate::config::BackendConfig::VaultTransit(vault_config) => (**vault_config).clone(), + crate::config::BackendConfig::VaultKv2(vault_config) => VaultTransitConfig { + address: vault_config.address.clone(), + auth_method: vault_config.auth_method.clone(), + namespace: vault_config.namespace.clone(), + mount_path: vault_config.mount_path.clone(), + tls: vault_config.tls.clone(), + }, + crate::config::BackendConfig::Local(_) => { + return Err(KmsError::configuration_error("Expected Vault Transit backend configuration")); + } + }; + + let client = VaultTransitKmsClient::new(vault_config).await?; + Ok(Self { client }) + } +} + +#[async_trait] +impl KmsBackend for VaultTransitKmsBackend { + async fn create_key(&self, request: CreateKeyRequest) -> Result { + let key_id = request.key_name.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + if self.client.read_transit_key(&key_id).await.is_ok() { + return Err(KmsError::key_already_exists(&key_id)); + } + + self.client.create_transit_key(&key_id).await?; + let metadata = TransitKeyMetadata::from_create_request(&request); + self.client.store_key_metadata(&key_id, &metadata).await?; + + Ok(CreateKeyResponse { + key_id: key_id.clone(), + key_metadata: KeyMetadata { + key_id, + key_state: metadata.key_state, + key_usage: metadata.key_usage, + description: metadata.description, + creation_date: metadata.created_at, + deletion_date: metadata.deletion_date, + origin: metadata.origin, + key_manager: "VAULT_TRANSIT".to_string(), + tags: metadata.tags, + }, + }) + } + + async fn encrypt(&self, request: EncryptRequest) -> Result { + self.client.encrypt(&request, None).await + } + + async fn decrypt(&self, request: DecryptRequest) -> Result { + let plaintext = self.client.decrypt(&request, None).await?; + Ok(DecryptResponse { + plaintext, + key_id: "vault-transit".to_string(), + encryption_algorithm: Some("vault-transit".to_string()), + }) + } + + async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result { + let generate_request = GenerateKeyRequest { + master_key_id: request.key_id.clone(), + key_spec: request.key_spec.as_str().to_string(), + key_length: Some(request.key_spec.key_size() as u32), + encryption_context: request.encryption_context, + grant_tokens: Vec::new(), + }; + + let data_key = self.client.generate_data_key(&generate_request, None).await?; + let plaintext_key = data_key.plaintext.clone().unwrap_or_default(); + let ciphertext_blob = data_key.ciphertext.clone(); + Ok(GenerateDataKeyResponse { + key_id: request.key_id, + plaintext_key, + ciphertext_blob, + }) + } + + async fn describe_key(&self, request: DescribeKeyRequest) -> Result { + Ok(DescribeKeyResponse { + key_metadata: self.client.key_metadata_response(&request.key_id).await?, + }) + } + + async fn list_keys(&self, request: ListKeysRequest) -> Result { + self.client.list_keys(&request, None).await + } + + async fn delete_key(&self, request: DeleteKeyRequest) -> Result { + let key_id = request.key_id; + let mut key_metadata = self.client.key_metadata_response(&key_id).await?; + + let deletion_date = if request.force_immediate.unwrap_or(false) { + if key_metadata.key_state == KeyState::PendingDeletion { + key::delete(&self.client.client, &self.client.config.mount_path, &key_id) + .await + .map_err(|e| KmsError::backend_error(format!("Failed to delete Vault Transit key {key_id}: {e}")))?; + self.client.delete_key_metadata(&key_id).await?; + None + } else { + let mut metadata = self.client.get_key_metadata(&key_id).await?; + metadata.key_state = KeyState::PendingDeletion; + metadata.deletion_date = Some(Zoned::now()); + self.client.store_key_metadata(&key_id, &metadata).await?; + key_metadata = self.client.key_metadata_response(&key_id).await?; + None + } + } else { + let days = request.pending_window_in_days.unwrap_or(30); + if !(7..=30).contains(&days) { + return Err(KmsError::invalid_parameter("pending_window_in_days must be between 7 and 30")); + } + + let mut metadata = self.client.get_key_metadata(&key_id).await?; + let scheduled = Zoned::now() + Duration::from_secs(days as u64 * 86400); + metadata.key_state = KeyState::PendingDeletion; + metadata.deletion_date = Some(scheduled.clone()); + self.client.store_key_metadata(&key_id, &metadata).await?; + key_metadata = self.client.key_metadata_response(&key_id).await?; + Some(scheduled.to_string()) + }; + + Ok(DeleteKeyResponse { + key_id, + deletion_date, + key_metadata, + }) + } + + async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result { + let mut metadata = self.client.get_key_metadata(&request.key_id).await?; + if metadata.key_state != KeyState::PendingDeletion { + return Err(KmsError::invalid_key_state(format!("Key {} is not pending deletion", request.key_id))); + } + + metadata.key_state = KeyState::Enabled; + metadata.deletion_date = None; + self.client.store_key_metadata(&request.key_id, &metadata).await?; + + Ok(CancelKeyDeletionResponse { + key_id: request.key_id.clone(), + key_metadata: self.client.key_metadata_response(&request.key_id).await?, + }) + } + + async fn health_check(&self) -> Result { + self.client.health_check().await.map(|_| true) + } +} diff --git a/crates/kms/src/config.rs b/crates/kms/src/config.rs index 177ccb473..e8721212c 100644 --- a/crates/kms/src/config.rs +++ b/crates/kms/src/config.rs @@ -24,8 +24,12 @@ use url::Url; /// KMS backend types #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum KmsBackend { - /// Vault backend (recommended for production) - Vault, + /// 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, @@ -69,8 +73,11 @@ impl Default for KmsConfig { pub enum BackendConfig { /// Local backend configuration Local(LocalConfig), - /// Vault backend configuration - Vault(Box), + /// Vault KV v2 + Transit backend configuration + #[serde(rename = "VaultKV2", alias = "Vault")] + VaultKv2(Box), + /// Vault Transit backend configuration + VaultTransit(Box), } impl Default for BackendConfig { @@ -100,7 +107,7 @@ impl Default for LocalConfig { } } -/// Vault backend configuration +/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VaultConfig { /// Vault server URL @@ -135,6 +142,35 @@ impl Default for VaultConfig { } } +/// 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 { @@ -194,8 +230,8 @@ impl KmsConfig { /// 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::Vault, - backend_config: BackendConfig::Vault(Box::new(VaultConfig { + backend: KmsBackend::VaultKv2, + backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::Token { token }, ..Default::default() @@ -207,8 +243,8 @@ impl KmsConfig { /// 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::Vault, - backend_config: BackendConfig::Vault(Box::new(VaultConfig { + backend: KmsBackend::VaultKv2, + backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::AppRole { role_id, secret_id }, ..Default::default() @@ -217,6 +253,19 @@ impl KmsConfig { } } + /// 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 { @@ -225,10 +274,18 @@ impl KmsConfig { } } - /// Get the Vault configuration if backend is Vault + /// Get the Vault KV2 configuration if backend is VaultKv2 pub fn vault_config(&self) -> Option<&VaultConfig> { match &self.backend_config { - BackendConfig::Vault(config) => Some(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, } } @@ -270,13 +327,13 @@ impl KmsConfig { return Err(KmsError::configuration_error("Local key directory must be an absolute path")); } } - BackendConfig::Vault(config) => { + BackendConfig::VaultKv2(config) => { if !config.address.starts_with("http://") && !config.address.starts_with("https://") { - return Err(KmsError::configuration_error("Vault address must use http or https scheme")); + 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 mount path cannot be empty")); + return Err(KmsError::configuration_error("Vault KV2 mount path cannot be empty")); } // Validate TLS configuration if using HTTPS @@ -290,6 +347,24 @@ impl KmsConfig { } } } + 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 @@ -308,7 +383,8 @@ impl KmsConfig { 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" => KmsBackend::Vault, + "vault" | "vault-kv2" | "vault_kv2" => KmsBackend::VaultKv2, + "vault-transit" | "vault_transit" => KmsBackend::VaultTransit, _ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {backend_type}"))), }; } @@ -348,11 +424,11 @@ impl KmsConfig { file_permissions: Some(0o600), }); } - KmsBackend::Vault => { + 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::Vault(Box::new(VaultConfig { + config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig { address, auth_method: VaultAuthMethod::Token { token }, namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"), @@ -362,6 +438,18 @@ impl KmsConfig { 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()?; @@ -399,13 +487,70 @@ mod tests { 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::Vault); + 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(); @@ -442,7 +587,7 @@ mod tests { || { let config = KmsConfig::from_env().expect("kms config should load from env"); - assert_eq!(config.backend, KmsBackend::Vault); + 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); @@ -457,4 +602,29 @@ mod tests { }, ); } + + #[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"); + }, + ); + } } diff --git a/crates/kms/src/lib.rs b/crates/kms/src/lib.rs index 7b8de98cc..8de6bc19d 100644 --- a/crates/kms/src/lib.rs +++ b/crates/kms/src/lib.rs @@ -20,7 +20,7 @@ //! //! ## Features //! -//! - **Multiple Backends**: Local file storage and Vault (optional) +//! - **Multiple Backends**: Local file storage, Vault KV2+Transit, and Vault Transit (optional) //! - **Object Encryption**: Transparent S3-compatible object encryption //! - **Streaming Encryption**: Memory-efficient encryption for large files //! - **Key Management**: Full lifecycle management of encryption keys @@ -29,7 +29,7 @@ //! ## Architecture //! //! The KMS follows a three-layer key hierarchy: -//! - **Master Keys**: Managed by KMS backends (Local/Vault) +//! - **Master Keys**: Managed by KMS backends (Local / Vault KV2 / Vault Transit) //! - **Data Encryption Keys (DEK)**: Generated per object, encrypted by master keys //! - **Object Data**: Encrypted using DEKs with AES-256-GCM or ChaCha20-Poly1305 //! @@ -71,8 +71,8 @@ pub mod types; // Re-export public API pub use api_types::{ CacheSummary, ConfigureKmsRequest, ConfigureKmsResponse, ConfigureLocalKmsRequest, ConfigureVaultKmsRequest, - KmsConfigSummary, KmsStatusResponse, StartKmsRequest, StartKmsResponse, StopKmsResponse, TagKeyRequest, TagKeyResponse, - UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse, + ConfigureVaultTransitKmsRequest, KmsConfigSummary, KmsStatusResponse, StartKmsRequest, StartKmsResponse, StopKmsResponse, + TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse, }; pub use config::*; pub use error::{KmsError, Result}; diff --git a/crates/kms/src/service_manager.rs b/crates/kms/src/service_manager.rs index f9cb2f676..7b850f2be 100644 --- a/crates/kms/src/service_manager.rs +++ b/crates/kms/src/service_manager.rs @@ -319,11 +319,16 @@ impl KmsServiceManager { let backend = LocalKmsBackend::new(config.clone()).await?; Arc::new(backend) as Arc } - BackendConfig::Vault(_) => { - info!("Creating Vault KMS backend for version {}", version); + BackendConfig::VaultKv2(_) => { + info!("Creating Vault KV2 KMS backend for version {}", version); let backend = crate::backends::vault::VaultKmsBackend::new(config.clone()).await?; Arc::new(backend) as Arc } + BackendConfig::VaultTransit(_) => { + info!("Creating Vault Transit KMS backend for version {}", version); + let backend = crate::backends::vault_transit::VaultTransitKmsBackend::new(config.clone()).await?; + Arc::new(backend) as Arc + } }; // Create KMS manager diff --git a/crates/kms/src/types.rs b/crates/kms/src/types.rs index 63e548ed2..39fda0ff4 100644 --- a/crates/kms/src/types.rs +++ b/crates/kms/src/types.rs @@ -626,7 +626,7 @@ pub struct HealthStatus { pub kms_healthy: bool, /// Whether encryption/decryption operations are working pub encryption_working: bool, - /// Backend type (e.g., "local", "vault") + /// Backend type (e.g., "local", "vault-kv2", "vault-transit") pub backend_type: String, /// Additional health details pub details: HashMap, diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index 3a88aad96..f9938912e 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -42,6 +42,48 @@ fn kms_service_manager_from_context() -> std::sync::Arc bool { + matches!( + auth_method, + rustfs_kms::config::VaultAuthMethod::Token { token } if token.trim().is_empty() + ) +} + +fn existing_vault_auth(config: &KmsConfig) -> Option { + match &config.backend_config { + rustfs_kms::config::BackendConfig::VaultKv2(vault) => Some(vault.auth_method.clone()), + rustfs_kms::config::BackendConfig::VaultTransit(vault) => Some(vault.auth_method.clone()), + rustfs_kms::config::BackendConfig::Local(_) => None, + } +} + +fn normalize_configure_request_auth( + request: &mut ConfigureKmsRequest, + existing_config: Option<&KmsConfig>, +) -> Result<(), String> { + let needs_existing_auth = match request { + ConfigureKmsRequest::VaultKv2(req) => token_is_blank(&req.auth_method), + ConfigureKmsRequest::VaultTransit(req) => token_is_blank(&req.auth_method), + ConfigureKmsRequest::Local(_) => false, + }; + + if !needs_existing_auth { + return Ok(()); + } + + let existing_auth = existing_config + .and_then(existing_vault_auth) + .ok_or_else(|| "Vault token is required when no existing KMS credentials are available".to_string())?; + + match request { + ConfigureKmsRequest::VaultKv2(req) => req.auth_method = existing_auth, + ConfigureKmsRequest::VaultTransit(req) => req.auth_method = existing_auth, + ConfigureKmsRequest::Local(_) => {} + } + + Ok(()) +} + /// Save KMS configuration to cluster storage #[instrument(skip(config))] async fn save_kms_config(config: &KmsConfig) -> Result<(), String> { @@ -153,7 +195,7 @@ impl Operation for ConfigureKmsHandler { .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; - let configure_request: ConfigureKmsRequest = if body.is_empty() { + let mut configure_request: ConfigureKmsRequest = if body.is_empty() { return Ok(S3Response::new(( StatusCode::BAD_REQUEST, Body::from("Request body is required".to_string()), @@ -171,6 +213,11 @@ impl Operation for ConfigureKmsHandler { info!("Configuring KMS with request: {:?}", configure_request); let service_manager = kms_service_manager_from_context(); + let existing_config = service_manager.get_config().await; + + if let Err(e) = normalize_configure_request_auth(&mut configure_request, existing_config.as_ref()) { + return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + } // Convert request to KmsConfig let kms_config = configure_request.to_kms_config(); @@ -508,7 +555,7 @@ impl Operation for ReconfigureKmsHandler { .await .map_err(|e| s3_error!(InvalidRequest, "failed to read request body: {}", e))?; - let configure_request: ConfigureKmsRequest = if body.is_empty() { + let mut configure_request: ConfigureKmsRequest = if body.is_empty() { return Ok(S3Response::new(( StatusCode::BAD_REQUEST, Body::from("Request body is required".to_string()), @@ -526,6 +573,11 @@ impl Operation for ReconfigureKmsHandler { info!("Reconfiguring KMS with request: {:?}", configure_request); let service_manager = kms_service_manager_from_context(); + let existing_config = service_manager.get_config().await; + + if let Err(e) = normalize_configure_request_auth(&mut configure_request, existing_config.as_ref()) { + return Ok(S3Response::new((StatusCode::BAD_REQUEST, Body::from(e)))); + } // Convert request to KmsConfig let kms_config = configure_request.to_kms_config(); diff --git a/rustfs/src/admin/handlers/kms_management.rs b/rustfs/src/admin/handlers/kms_management.rs index c91c4644f..29599cf69 100644 --- a/rustfs/src/admin/handlers/kms_management.rs +++ b/rustfs/src/admin/handlers/kms_management.rs @@ -22,7 +22,7 @@ use crate::auth::{check_key_valid, get_session_token}; use crate::server::{ADMIN_PREFIX, RemoteAddr}; use hyper::{HeaderMap, Method, StatusCode}; use matchit::Params; -use rustfs_kms::init_global_kms_service_manager; +use rustfs_kms::{KmsBackend, init_global_kms_service_manager}; use rustfs_policy::policy::action::{Action, AdminAction}; use s3s::header::CONTENT_TYPE; use s3s::{Body, S3Request, S3Response, S3Result, s3_error}; @@ -30,14 +30,26 @@ use serde::{Deserialize, Serialize}; use tracing::{error, info, warn}; async fn kms_encryption_service_from_context() -> Option> { - let manager = match resolve_kms_runtime_service_manager() { + let manager = kms_service_manager_from_context(); + manager.get_encryption_service().await +} + +fn kms_service_manager_from_context() -> std::sync::Arc { + match resolve_kms_runtime_service_manager() { Some(manager) => manager, None => { warn!("KMS service manager not initialized, initializing now as fallback"); init_global_kms_service_manager() } - }; - manager.get_encryption_service().await + } +} + +fn backend_name(backend: &KmsBackend) -> &'static str { + match backend { + KmsBackend::Local => "local", + KmsBackend::VaultKv2 => "vault-kv2", + KmsBackend::VaultTransit => "vault-transit", + } } #[derive(Debug, Serialize, Deserialize)] @@ -168,11 +180,15 @@ impl Operation for KmsStatusHandler { hit_count: hits, miss_count: misses, }); + let config = kms_service_manager_from_context().get_config().await; let response = KmsStatusResponse { - backend_type: "vault".to_string(), // TODO: Get from config + backend_type: config + .as_ref() + .map(|cfg| backend_name(&cfg.backend).to_string()) + .unwrap_or_else(|| "unknown".to_string()), backend_status, - cache_enabled: cache_stats.is_some(), + cache_enabled: config.as_ref().is_some_and(|cfg| cfg.enable_cache), cache_stats, default_key_id: service.get_default_key_id().cloned(), }; @@ -213,12 +229,16 @@ impl Operation for KmsConfigHandler { return Err(s3_error!(InternalError, "KMS service not initialized")); }; - // TODO: Get actual config from service + let config = kms_service_manager_from_context() + .get_config() + .await + .ok_or_else(|| s3_error!(InternalError, "KMS config not available"))?; + let response = KmsConfigResponse { - backend: "vault".to_string(), - cache_enabled: true, - cache_max_keys: 1000, - cache_ttl_seconds: 300, + backend: backend_name(&config.backend).to_string(), + cache_enabled: config.enable_cache, + cache_max_keys: config.cache_config.max_keys, + cache_ttl_seconds: config.cache_config.ttl.as_secs(), default_key_id: service.get_default_key_id().cloned(), }; diff --git a/rustfs/src/config/cli.rs b/rustfs/src/config/cli.rs index 96d07a1e2..559402e10 100644 --- a/rustfs/src/config/cli.rs +++ b/rustfs/src/config/cli.rs @@ -218,7 +218,7 @@ pub struct ServerOpts { #[arg(long, default_value_t = false, env = "RUSTFS_KMS_ENABLE")] pub kms_enable: bool, - /// KMS backend type (local or vault) + /// KMS backend type: local, vault or vault-kv2 (Vault KV2+Transit), vault-transit #[arg(long, default_value_t = rustfs_config::DEFAULT_KMS_BACKEND.to_string(), env = "RUSTFS_KMS_BACKEND")] pub kms_backend: String, @@ -234,6 +234,10 @@ pub struct ServerOpts { #[arg(long, env = "RUSTFS_KMS_VAULT_TOKEN")] pub kms_vault_token: Option, + /// Vault mount path for vault or vault-transit backend + #[arg(long, env = "RUSTFS_KMS_VAULT_MOUNT_PATH")] + pub kms_vault_mount_path: Option, + /// Default KMS key ID for encryption #[arg(long, env = "RUSTFS_KMS_DEFAULT_KEY_ID")] pub kms_default_key_id: Option, @@ -284,6 +288,7 @@ pub fn default_server_opts() -> ServerOpts { kms_key_dir: None, kms_vault_address: None, kms_vault_token: None, + kms_vault_mount_path: None, kms_default_key_id: None, 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 0d4f730cd..74fbe5183 100644 --- a/rustfs/src/config/config_struct.rs +++ b/rustfs/src/config/config_struct.rs @@ -97,6 +97,9 @@ pub struct Config { /// Vault token for vault backend pub kms_vault_token: Option, + /// Vault mount path for vault or vault-transit backend + pub kms_vault_mount_path: Option, + /// Default KMS key ID for encryption pub kms_default_key_id: Option, @@ -129,6 +132,7 @@ impl Config { kms_key_dir, kms_vault_address, kms_vault_token, + kms_vault_mount_path, kms_default_key_id, buffer_profile_disable, buffer_profile, @@ -157,6 +161,7 @@ impl Config { kms_key_dir, kms_vault_address, kms_vault_token, + kms_vault_mount_path, kms_default_key_id, buffer_profile_disable, buffer_profile, @@ -197,6 +202,7 @@ impl std::fmt::Debug for Config { .field("kms_key_dir", &self.kms_key_dir) .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("buffer_profile_disable", &self.buffer_profile_disable) .field("buffer_profile", &self.buffer_profile) diff --git a/rustfs/src/config/opt.rs b/rustfs/src/config/opt.rs index f3ba22e5d..c3d50ca2e 100644 --- a/rustfs/src/config/opt.rs +++ b/rustfs/src/config/opt.rs @@ -46,6 +46,7 @@ pub struct Opt { pub kms_key_dir: Option, pub kms_vault_address: Option, pub kms_vault_token: Option, + pub kms_vault_mount_path: Option, pub kms_default_key_id: Option, pub buffer_profile_disable: bool, pub buffer_profile: String, @@ -73,6 +74,7 @@ impl Opt { kms_key_dir: o.kms_key_dir, 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, 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 1c7e56736..172330b65 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -196,14 +196,14 @@ fn build_vault_kms_config(cfg: &config::Config) -> std::io::Result std::io::Result std::io::Result { + let vault_address = cfg + .kms_vault_address + .as_ref() + .ok_or_else(|| Error::other("Vault address is required for vault-transit backend"))?; + let vault_token = cfg + .kms_vault_token + .as_ref() + .ok_or_else(|| Error::other("Vault token is required for vault-transit backend"))?; + + Ok(rustfs_kms::config::KmsConfig { + backend: rustfs_kms::config::KmsBackend::VaultTransit, + backend_config: rustfs_kms::config::BackendConfig::VaultTransit(Box::new(rustfs_kms::config::VaultTransitConfig { + address: vault_address.clone(), + auth_method: rustfs_kms::config::VaultAuthMethod::Token { + token: vault_token.clone(), + }, + namespace: None, + mount_path: cfg.kms_vault_mount_path.clone().unwrap_or_else(|| "transit".to_string()), + tls: None, + })), + default_key_id: cfg.kms_default_key_id.clone(), + timeout: std::time::Duration::from_secs(30), + retry_attempts: 3, + enable_cache: true, + cache_config: rustfs_kms::config::CacheConfig::default(), + }) +} + /// Configure and start KMS service async fn configure_and_start_kms( service_manager: &std::sync::Arc, @@ -258,7 +288,8 @@ pub(crate) async fn init_kms_system(config: &config::Config) -> std::io::Result< // Create KMS configuration from command line options let kms_config = match config.kms_backend.as_str() { "local" => build_local_kms_config(config)?, - "vault" => build_vault_kms_config(config)?, + "vault" | "vault-kv2" | "vault_kv2" => build_vault_kms_config(config)?, + "vault-transit" | "vault_transit" => build_vault_transit_kms_config(config)?, _ => return Err(Error::other(format!("Unsupported KMS backend: {}", config.kms_backend))), }; From 3a16c25cd97f7f1084d354df34098e0b7a33034b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Tue, 7 Apr 2026 13:52:48 +0800 Subject: [PATCH 2/4] Update crates/kms/src/backends/vault_transit.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: 唐小鸭 --- crates/kms/src/backends/vault_transit.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/kms/src/backends/vault_transit.rs b/crates/kms/src/backends/vault_transit.rs index 08a3e03e7..a04d99e1f 100644 --- a/crates/kms/src/backends/vault_transit.rs +++ b/crates/kms/src/backends/vault_transit.rs @@ -535,10 +535,11 @@ impl KmsBackend for VaultTransitKmsBackend { } async fn decrypt(&self, request: DecryptRequest) -> Result { + let envelope = DataKeyEnvelope::deserialize(&request.ciphertext_blob)?; let plaintext = self.client.decrypt(&request, None).await?; Ok(DecryptResponse { plaintext, - key_id: "vault-transit".to_string(), + key_id: envelope.master_key_id, encryption_algorithm: Some("vault-transit".to_string()), }) } From 55f175b671235290de6d01d2cd9f209d56288266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Tue, 7 Apr 2026 13:53:05 +0800 Subject: [PATCH 3/4] Update rustfs/src/admin/handlers/kms_dynamic.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: 唐小鸭 --- rustfs/src/admin/handlers/kms_dynamic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index f9938912e..402984576 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -210,7 +210,7 @@ impl Operation for ConfigureKmsHandler { } }; - info!("Configuring KMS with request: {:?}", configure_request); + info!("Configuring KMS from admin request"); let service_manager = kms_service_manager_from_context(); let existing_config = service_manager.get_config().await; From 72928a43fae4c2f9d532dea71247e0e81b11237b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Tue, 7 Apr 2026 13:53:19 +0800 Subject: [PATCH 4/4] Update rustfs/src/admin/handlers/kms_dynamic.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: 唐小鸭 --- rustfs/src/admin/handlers/kms_dynamic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index 402984576..557c1e56a 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -570,7 +570,7 @@ impl Operation for ReconfigureKmsHandler { } }; - info!("Reconfiguring KMS with request: {:?}", configure_request); + info!("Reconfiguring KMS"); let service_manager = kms_service_manager_from_context(); let existing_config = service_manager.get_config().await;