mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
chore: merge upstream PR 2372 fixes
This commit is contained in:
Vendored
+14
-6
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let base_env = RustFSTestEnvironment::new().await?;
|
||||
@@ -399,7 +404,8 @@ impl VaultTestEnvironment {
|
||||
pub async fn start_vault(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<dyn std::error::Error + Send + Sync>> {
|
||||
/// Configure Vault Transit KMS backend
|
||||
pub async fn configure_vault_transit_kms(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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()),
|
||||
|
||||
+244
-12
@@ -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<u64>,
|
||||
}
|
||||
|
||||
/// 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,56 @@ pub struct ConfigureVaultKmsRequest {
|
||||
pub cache_ttl_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
/// Transit engine mount path
|
||||
pub mount_path: Option<String>,
|
||||
/// Skip TLS verification (insecure, for development only)
|
||||
pub skip_tls_verify: Option<bool>,
|
||||
/// Default master key ID for auto-encryption
|
||||
pub default_key_id: Option<String>,
|
||||
/// Operation timeout in seconds
|
||||
pub timeout_seconds: Option<u64>,
|
||||
/// Number of retry attempts
|
||||
pub retry_attempts: Option<u32>,
|
||||
/// Enable caching
|
||||
pub enable_cache: Option<bool>,
|
||||
/// Maximum number of keys to cache
|
||||
pub max_cached_keys: Option<usize>,
|
||||
/// Cache TTL in seconds
|
||||
pub cache_ttl_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
#[serde(alias = "local", alias = "Local")]
|
||||
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
|
||||
#[serde(
|
||||
rename = "VaultTransit",
|
||||
alias = "vault-transit",
|
||||
alias = "vault_transit"
|
||||
)]
|
||||
VaultTransit(ConfigureVaultTransitKmsRequest),
|
||||
}
|
||||
|
||||
/// KMS configuration response
|
||||
@@ -152,6 +196,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<CacheSummary>,
|
||||
/// Backend-specific summary
|
||||
@@ -171,7 +219,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 +230,15 @@ pub enum BackendSummary {
|
||||
/// File permissions (octal)
|
||||
file_permissions: Option<u32>,
|
||||
},
|
||||
/// 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<String>,
|
||||
/// Transit engine mount path
|
||||
@@ -196,6 +247,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<String>,
|
||||
/// Transit engine mount path
|
||||
mount_path: String,
|
||||
/// Skip TLS verification
|
||||
skip_tls_verify: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -217,16 +285,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 +317,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 +352,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 +384,161 @@ 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 cases = ["VaultTransit", "vault-transit", "vault_transit"];
|
||||
for raw_backend in cases {
|
||||
let raw = serde_json::json!({
|
||||
"backend_type": raw_backend,
|
||||
"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_deserialize_local_configure_request() {
|
||||
let raw = serde_json::json!({
|
||||
"backend_type": "local",
|
||||
"key_dir": "./target/kms-key-dir"
|
||||
});
|
||||
|
||||
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::Local);
|
||||
}
|
||||
|
||||
#[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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +566,9 @@ impl LocalKmsBackend {
|
||||
pub async fn new(config: KmsConfig) -> Result<Self> {
|
||||
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?;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<Self> {
|
||||
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?;
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
// 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<String>,
|
||||
tags: HashMap<String, String>,
|
||||
key_state: KeyState,
|
||||
created_at: Zoned,
|
||||
deletion_date: Option<Zoned>,
|
||||
origin: String,
|
||||
created_by: Option<String>,
|
||||
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<HashMap<String, TransitKeyMetadata>>,
|
||||
}
|
||||
|
||||
impl VaultTransitKmsClient {
|
||||
pub async fn new(config: VaultTransitConfig) -> Result<Self> {
|
||||
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<String, String>) -> Result<Option<String>> {
|
||||
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<T>(key_id: &str, error: vaultrs::error::ClientError, operation: &str) -> Result<T> {
|
||||
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<vaultrs::api::transit::responses::ReadKeyResponse> {
|
||||
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<String, String>,
|
||||
) -> Result<String> {
|
||||
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<String, String>,
|
||||
) -> Result<Vec<u8>> {
|
||||
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<TransitKeyMetadata> {
|
||||
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<KeyInfo> {
|
||||
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<KeyMetadata> {
|
||||
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<TransitKeyMetadata> {
|
||||
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<DataKeyInfo> {
|
||||
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<EncryptResponse> {
|
||||
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<Vec<u8>> {
|
||||
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<MasterKeyInfo> {
|
||||
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<KeyInfo> {
|
||||
self.key_info(key_id).await
|
||||
}
|
||||
|
||||
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||
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<MasterKeyInfo> {
|
||||
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<Self> {
|
||||
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<CreateKeyResponse> {
|
||||
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<EncryptResponse> {
|
||||
self.client.encrypt(&request, None).await
|
||||
}
|
||||
|
||||
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)?;
|
||||
let plaintext = self.client.decrypt(&request, None).await?;
|
||||
Ok(DecryptResponse {
|
||||
plaintext,
|
||||
key_id: envelope.master_key_id,
|
||||
encryption_algorithm: Some("vault-transit".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||
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<DescribeKeyResponse> {
|
||||
Ok(DescribeKeyResponse {
|
||||
key_metadata: self.client.key_metadata_response(&request.key_id).await?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
|
||||
self.client.list_keys(&request, None).await
|
||||
}
|
||||
|
||||
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
|
||||
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<CancelKeyDeletionResponse> {
|
||||
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<bool> {
|
||||
self.client.health_check().await.map(|_| true)
|
||||
}
|
||||
}
|
||||
+189
-19
@@ -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<VaultConfig>),
|
||||
/// Vault KV v2 + Transit backend configuration
|
||||
#[serde(rename = "VaultKV2", alias = "Vault")]
|
||||
VaultKv2(Box<VaultConfig>),
|
||||
/// Vault Transit backend configuration
|
||||
VaultTransit(Box<VaultTransitConfig>),
|
||||
}
|
||||
|
||||
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<String>,
|
||||
/// Transit engine mount path
|
||||
pub mount_path: String,
|
||||
/// TLS configuration
|
||||
pub tls: Option<TlsConfig>,
|
||||
}
|
||||
|
||||
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");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -319,11 +319,16 @@ impl KmsServiceManager {
|
||||
let backend = LocalKmsBackend::new(config.clone()).await?;
|
||||
Arc::new(backend) as Arc<dyn KmsBackend>
|
||||
}
|
||||
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<dyn KmsBackend>
|
||||
}
|
||||
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<dyn KmsBackend>
|
||||
}
|
||||
};
|
||||
|
||||
// Create KMS manager
|
||||
|
||||
@@ -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<String, String>,
|
||||
|
||||
@@ -42,6 +42,48 @@ fn kms_service_manager_from_context() -> std::sync::Arc<rustfs_kms::KmsServiceMa
|
||||
})
|
||||
}
|
||||
|
||||
fn token_is_blank(auth_method: &rustfs_kms::config::VaultAuthMethod) -> bool {
|
||||
matches!(
|
||||
auth_method,
|
||||
rustfs_kms::config::VaultAuthMethod::Token { token } if token.trim().is_empty()
|
||||
)
|
||||
}
|
||||
|
||||
fn existing_vault_auth(config: &KmsConfig) -> Option<rustfs_kms::config::VaultAuthMethod> {
|
||||
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()),
|
||||
@@ -168,9 +210,14 @@ 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;
|
||||
|
||||
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()),
|
||||
@@ -523,9 +570,14 @@ 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;
|
||||
|
||||
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();
|
||||
|
||||
@@ -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<std::sync::Arc<rustfs_kms::ObjectEncryptionService>> {
|
||||
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<rustfs_kms::KmsServiceManager> {
|
||||
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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
/// Vault mount path for vault or vault-transit backend
|
||||
#[arg(long, env = "RUSTFS_KMS_VAULT_MOUNT_PATH")]
|
||||
pub kms_vault_mount_path: Option<String>,
|
||||
|
||||
/// Default KMS key ID for encryption
|
||||
#[arg(long, env = "RUSTFS_KMS_DEFAULT_KEY_ID")]
|
||||
pub kms_default_key_id: Option<String>,
|
||||
@@ -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(),
|
||||
|
||||
@@ -97,6 +97,9 @@ pub struct Config {
|
||||
/// Vault token for vault backend
|
||||
pub kms_vault_token: Option<String>,
|
||||
|
||||
/// Vault mount path for vault or vault-transit backend
|
||||
pub kms_vault_mount_path: Option<String>,
|
||||
|
||||
/// Default KMS key ID for encryption
|
||||
pub kms_default_key_id: Option<String>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -46,6 +46,7 @@ pub struct Opt {
|
||||
pub kms_key_dir: Option<String>,
|
||||
pub kms_vault_address: Option<String>,
|
||||
pub kms_vault_token: Option<String>,
|
||||
pub kms_vault_mount_path: Option<String>,
|
||||
pub kms_default_key_id: Option<String>,
|
||||
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,
|
||||
|
||||
+35
-4
@@ -196,14 +196,14 @@ fn build_vault_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
||||
.ok_or_else(|| Error::other("Vault token is required for vault backend"))?;
|
||||
|
||||
Ok(rustfs_kms::config::KmsConfig {
|
||||
backend: rustfs_kms::config::KmsBackend::Vault,
|
||||
backend_config: rustfs_kms::config::BackendConfig::Vault(Box::new(rustfs_kms::config::VaultConfig {
|
||||
backend: rustfs_kms::config::KmsBackend::VaultKv2,
|
||||
backend_config: rustfs_kms::config::BackendConfig::VaultKv2(Box::new(rustfs_kms::config::VaultConfig {
|
||||
address: vault_address.clone(),
|
||||
auth_method: rustfs_kms::config::VaultAuthMethod::Token {
|
||||
token: vault_token.clone(),
|
||||
},
|
||||
namespace: None,
|
||||
mount_path: "transit".to_string(),
|
||||
mount_path: cfg.kms_vault_mount_path.clone().unwrap_or_else(|| "transit".to_string()),
|
||||
kv_mount: "secret".to_string(),
|
||||
key_path_prefix: "rustfs/kms/keys".to_string(),
|
||||
tls: None,
|
||||
@@ -216,6 +216,36 @@ fn build_vault_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
||||
})
|
||||
}
|
||||
|
||||
/// Build KMS configuration for Vault Transit backend
|
||||
fn build_vault_transit_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::config::KmsConfig> {
|
||||
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<rustfs_kms::KmsServiceManager>,
|
||||
@@ -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))),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user