mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 01:38:18 +00:00
feat(kms): add vault transit engine
This commit is contained in:
+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");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user