mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 19:16:17 +00:00
feat: add KMS config redaction safeguards (#3303)
This commit is contained in:
@@ -57,6 +57,7 @@ moka = { workspace = true, features = ["future"] }
|
||||
md5 = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
rustfs-utils = { workspace = true }
|
||||
rustfs-security-governance = { workspace = true }
|
||||
|
||||
# HTTP client for Vault
|
||||
reqwest = { workspace = true }
|
||||
|
||||
+119
-1
@@ -16,16 +16,18 @@
|
||||
|
||||
use crate::config::{
|
||||
BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig,
|
||||
redacted_secret_option,
|
||||
};
|
||||
use crate::service_manager::KmsServiceStatus;
|
||||
use crate::types::{KeyMetadata, KeyUsage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Request to configure KMS with Local backend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigureLocalKmsRequest {
|
||||
/// Directory to store key files
|
||||
pub key_dir: PathBuf,
|
||||
@@ -47,6 +49,23 @@ pub struct ConfigureLocalKmsRequest {
|
||||
pub cache_ttl_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ConfigureLocalKmsRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let master_key = redacted_secret_option(self.master_key.as_deref());
|
||||
f.debug_struct("ConfigureLocalKmsRequest")
|
||||
.field("key_dir", &self.key_dir)
|
||||
.field("master_key", &master_key)
|
||||
.field("file_permissions", &self.file_permissions)
|
||||
.field("default_key_id", &self.default_key_id)
|
||||
.field("timeout_seconds", &self.timeout_seconds)
|
||||
.field("retry_attempts", &self.retry_attempts)
|
||||
.field("enable_cache", &self.enable_cache)
|
||||
.field("max_cached_keys", &self.max_cached_keys)
|
||||
.field("cache_ttl_seconds", &self.cache_ttl_seconds)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to configure KMS with Vault KV v2 + Transit backend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigureVaultKmsRequest {
|
||||
@@ -428,6 +447,7 @@ impl ConfigureKmsRequest {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::REDACTED_SECRET;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() {
|
||||
@@ -536,6 +556,104 @@ mod tests {
|
||||
other => panic!("expected vault-transit summary, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_configure_request_debug_redacts_kms_secret_fields() {
|
||||
let local = ConfigureKmsRequest::Local(ConfigureLocalKmsRequest {
|
||||
key_dir: PathBuf::from("/tmp/kms"),
|
||||
master_key: Some("local-configure-master-secret".to_string()),
|
||||
file_permissions: Some(0o600),
|
||||
default_key_id: Some("default-key".to_string()),
|
||||
timeout_seconds: Some(30),
|
||||
retry_attempts: Some(3),
|
||||
enable_cache: Some(true),
|
||||
max_cached_keys: Some(16),
|
||||
cache_ttl_seconds: Some(60),
|
||||
});
|
||||
let vault = ConfigureKmsRequest::VaultTransit(ConfigureVaultTransitKmsRequest {
|
||||
address: "https://vault.example.com:8200".to_string(),
|
||||
auth_method: VaultAuthMethod::Token {
|
||||
token: "configure-vault-token-secret".to_string(),
|
||||
},
|
||||
namespace: None,
|
||||
mount_path: Some("transit".to_string()),
|
||||
skip_tls_verify: Some(false),
|
||||
default_key_id: None,
|
||||
timeout_seconds: None,
|
||||
retry_attempts: None,
|
||||
enable_cache: None,
|
||||
max_cached_keys: None,
|
||||
cache_ttl_seconds: None,
|
||||
});
|
||||
let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest {
|
||||
address: "https://vault.example.com:8200".to_string(),
|
||||
auth_method: VaultAuthMethod::AppRole {
|
||||
role_id: "configure-role-id".to_string(),
|
||||
secret_id: "configure-approle-secret-id".to_string(),
|
||||
},
|
||||
namespace: None,
|
||||
mount_path: Some("transit".to_string()),
|
||||
kv_mount: Some("secret".to_string()),
|
||||
key_path_prefix: Some("rustfs/kms/keys".to_string()),
|
||||
skip_tls_verify: Some(false),
|
||||
default_key_id: None,
|
||||
timeout_seconds: None,
|
||||
retry_attempts: None,
|
||||
enable_cache: None,
|
||||
max_cached_keys: None,
|
||||
cache_ttl_seconds: None,
|
||||
});
|
||||
|
||||
let rendered = format!("{local:?}\n{vault:?}\n{approle:?}");
|
||||
|
||||
assert!(!rendered.contains("local-configure-master-secret"));
|
||||
assert!(!rendered.contains("configure-vault-token-secret"));
|
||||
assert!(!rendered.contains("configure-approle-secret-id"));
|
||||
assert!(rendered.contains("configure-role-id"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kms_status_response_omits_secret_values_from_json_and_debug() {
|
||||
let configs = [
|
||||
KmsConfig {
|
||||
backend: KmsBackend::Local,
|
||||
backend_config: BackendConfig::Local(LocalConfig {
|
||||
key_dir: PathBuf::from("/tmp/kms"),
|
||||
master_key: Some("local-summary-master-secret".to_string()),
|
||||
file_permissions: Some(0o600),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
KmsConfig::vault(
|
||||
url::Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||
"summary-vault-token-secret".to_string(),
|
||||
),
|
||||
KmsConfig::vault_approle(
|
||||
url::Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||
"summary-role-id".to_string(),
|
||||
"summary-approle-secret-id".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
for config in configs {
|
||||
let summary = KmsConfigSummary::from(&config);
|
||||
let response = KmsStatusResponse {
|
||||
status: KmsServiceStatus::Configured,
|
||||
backend_type: Some(config.backend.clone()),
|
||||
healthy: None,
|
||||
config_summary: Some(summary),
|
||||
};
|
||||
let json = serde_json::to_string(&response).expect("kms status response should serialize");
|
||||
let debug = format!("{response:?}");
|
||||
let rendered = format!("{json}\n{debug}");
|
||||
|
||||
assert!(!rendered.contains("local-summary-master-secret"));
|
||||
assert!(!rendered.contains("summary-vault-token-secret"));
|
||||
assert!(!rendered.contains("summary-approle-secret-id"));
|
||||
assert!(rendered.contains("has_master_key") || rendered.contains("has_stored_credentials"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
+162
-5
@@ -15,12 +15,61 @@
|
||||
//! KMS configuration management
|
||||
|
||||
use crate::error::{KmsError, Result};
|
||||
use rustfs_security_governance::{RedactionLevel, RedactionRule};
|
||||
use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
|
||||
RedactionRule::new("kms.local.master_key", RedactionLevel::Secret, "local backend key encryption material"),
|
||||
RedactionRule::new("kms.vault.token", RedactionLevel::Secret, "vault authentication token"),
|
||||
RedactionRule::new("kms.vault.approle.secret_id", RedactionLevel::Secret, "vault approle secret"),
|
||||
RedactionRule::new("kms.vault_transit.token", RedactionLevel::Secret, "vault transit authentication token"),
|
||||
RedactionRule::new(
|
||||
"kms.vault_transit.approle.secret_id",
|
||||
RedactionLevel::Secret,
|
||||
"vault transit approle secret",
|
||||
),
|
||||
RedactionRule::new(
|
||||
"kms.configure.local.master_key",
|
||||
RedactionLevel::Secret,
|
||||
"admin configure request local master key",
|
||||
),
|
||||
RedactionRule::new(
|
||||
"kms.configure.vault.token",
|
||||
RedactionLevel::Secret,
|
||||
"admin configure request vault authentication token",
|
||||
),
|
||||
RedactionRule::new(
|
||||
"kms.configure.vault.approle.secret_id",
|
||||
RedactionLevel::Secret,
|
||||
"admin configure request vault approle secret",
|
||||
),
|
||||
RedactionRule::new(
|
||||
"kms.configure.vault_transit.token",
|
||||
RedactionLevel::Secret,
|
||||
"admin configure request vault transit authentication token",
|
||||
),
|
||||
RedactionRule::new(
|
||||
"kms.configure.vault_transit.approle.secret_id",
|
||||
RedactionLevel::Secret,
|
||||
"admin configure request vault transit approle secret",
|
||||
),
|
||||
];
|
||||
|
||||
pub(crate) const REDACTED_SECRET: &str = "***redacted***";
|
||||
|
||||
pub(crate) fn redacted_secret(value: &str) -> &'static str {
|
||||
if value.is_empty() { "" } else { REDACTED_SECRET }
|
||||
}
|
||||
|
||||
pub(crate) fn redacted_secret_option(value: Option<&str>) -> Option<&'static str> {
|
||||
value.map(redacted_secret)
|
||||
}
|
||||
|
||||
/// KMS backend types
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum KmsBackend {
|
||||
@@ -69,7 +118,7 @@ impl Default for KmsConfig {
|
||||
}
|
||||
|
||||
/// Backend-specific configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum BackendConfig {
|
||||
/// Local backend configuration
|
||||
Local(LocalConfig),
|
||||
@@ -86,8 +135,18 @@ impl Default for BackendConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BackendConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Local(config) => f.debug_tuple("Local").field(config).finish(),
|
||||
Self::VaultKv2(config) => f.debug_tuple("VaultKv2").field(config).finish(),
|
||||
Self::VaultTransit(config) => f.debug_tuple("VaultTransit").field(config).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Local KMS backend configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct LocalConfig {
|
||||
/// Directory to store key files
|
||||
pub key_dir: PathBuf,
|
||||
@@ -97,6 +156,17 @@ pub struct LocalConfig {
|
||||
pub file_permissions: Option<u32>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for LocalConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let master_key = redacted_secret_option(self.master_key.as_deref());
|
||||
f.debug_struct("LocalConfig")
|
||||
.field("key_dir", &self.key_dir)
|
||||
.field("master_key", &master_key)
|
||||
.field("file_permissions", &self.file_permissions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LocalConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -108,7 +178,7 @@ impl Default for LocalConfig {
|
||||
}
|
||||
|
||||
/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct VaultConfig {
|
||||
/// Vault server URL
|
||||
pub address: String,
|
||||
@@ -126,6 +196,20 @@ pub struct VaultConfig {
|
||||
pub tls: Option<TlsConfig>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for VaultConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("VaultConfig")
|
||||
.field("address", &self.address)
|
||||
.field("auth_method", &self.auth_method)
|
||||
.field("namespace", &self.namespace)
|
||||
.field("mount_path", &self.mount_path)
|
||||
.field("kv_mount", &self.kv_mount)
|
||||
.field("key_path_prefix", &self.key_path_prefix)
|
||||
.field("tls", &self.tls)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VaultConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -143,7 +227,7 @@ impl Default for VaultConfig {
|
||||
}
|
||||
|
||||
/// Vault Transit backend configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct VaultTransitConfig {
|
||||
/// Vault server URL
|
||||
pub address: String,
|
||||
@@ -157,6 +241,18 @@ pub struct VaultTransitConfig {
|
||||
pub tls: Option<TlsConfig>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for VaultTransitConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("VaultTransitConfig")
|
||||
.field("address", &self.address)
|
||||
.field("auth_method", &self.auth_method)
|
||||
.field("namespace", &self.namespace)
|
||||
.field("mount_path", &self.mount_path)
|
||||
.field("tls", &self.tls)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VaultTransitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -172,7 +268,7 @@ impl Default for VaultTransitConfig {
|
||||
}
|
||||
|
||||
/// Vault authentication methods
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum VaultAuthMethod {
|
||||
/// Token authentication
|
||||
Token { token: String },
|
||||
@@ -180,6 +276,19 @@ pub enum VaultAuthMethod {
|
||||
AppRole { role_id: String, secret_id: String },
|
||||
}
|
||||
|
||||
impl fmt::Debug for VaultAuthMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Token { token } => f.debug_struct("Token").field("token", &redacted_secret(token)).finish(),
|
||||
Self::AppRole { role_id, secret_id } => f
|
||||
.debug_struct("AppRole")
|
||||
.field("role_id", role_id)
|
||||
.field("secret_id", &redacted_secret(secret_id))
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS configuration for Vault
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlsConfig {
|
||||
@@ -460,6 +569,7 @@ impl KmsConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rustfs_security_governance::validate_redaction_rules;
|
||||
use temp_env::with_vars;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -551,6 +661,53 @@ mod tests {
|
||||
assert_eq!(serialized, "\"VaultTransit\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kms_redaction_rules_are_valid() {
|
||||
assert!(validate_redaction_rules(KMS_CONFIG_REDACTION_RULES).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kms_config_debug_redacts_secret_fields() {
|
||||
let local = KmsConfig {
|
||||
backend: KmsBackend::Local,
|
||||
backend_config: BackendConfig::Local(LocalConfig {
|
||||
key_dir: PathBuf::from("/tmp/kms"),
|
||||
master_key: Some("local-master-secret".to_string()),
|
||||
file_permissions: Some(0o600),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let vault = KmsConfig::vault(
|
||||
Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||
"vault-token-secret".to_string(),
|
||||
);
|
||||
let approle = KmsConfig::vault_approle(
|
||||
Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||
"role-id-visible".to_string(),
|
||||
"approle-secret-id".to_string(),
|
||||
);
|
||||
|
||||
let rendered = format!("{local:?}\n{vault:?}\n{approle:?}");
|
||||
|
||||
assert!(!rendered.contains("local-master-secret"));
|
||||
assert!(!rendered.contains("vault-token-secret"));
|
||||
assert!(!rendered.contains("approle-secret-id"));
|
||||
assert!(rendered.contains("role-id-visible"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kms_config_serialization_preserves_secret_fields_for_persistence() {
|
||||
let config = KmsConfig::vault(
|
||||
Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||
"persisted-token-secret".to_string(),
|
||||
);
|
||||
|
||||
let serialized = serde_json::to_string(&config).expect("kms config should serialize for persistence");
|
||||
|
||||
assert!(serialized.contains("persisted-token-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation() {
|
||||
let mut config = KmsConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user