mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 07:57:01 +00:00
feat: add KMS config redaction safeguards (#3303)
This commit is contained in:
Generated
+1
@@ -9564,6 +9564,7 @@ dependencies = [
|
|||||||
"moka",
|
"moka",
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustfs-security-governance",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ moka = { workspace = true, features = ["future"] }
|
|||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
arc-swap = { workspace = true }
|
arc-swap = { workspace = true }
|
||||||
rustfs-utils = { workspace = true }
|
rustfs-utils = { workspace = true }
|
||||||
|
rustfs-security-governance = { workspace = true }
|
||||||
|
|
||||||
# HTTP client for Vault
|
# HTTP client for Vault
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
|
|||||||
+119
-1
@@ -16,16 +16,18 @@
|
|||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig,
|
BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig,
|
||||||
|
redacted_secret_option,
|
||||||
};
|
};
|
||||||
use crate::service_manager::KmsServiceStatus;
|
use crate::service_manager::KmsServiceStatus;
|
||||||
use crate::types::{KeyMetadata, KeyUsage};
|
use crate::types::{KeyMetadata, KeyUsage};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Request to configure KMS with Local backend
|
/// Request to configure KMS with Local backend
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct ConfigureLocalKmsRequest {
|
pub struct ConfigureLocalKmsRequest {
|
||||||
/// Directory to store key files
|
/// Directory to store key files
|
||||||
pub key_dir: PathBuf,
|
pub key_dir: PathBuf,
|
||||||
@@ -47,6 +49,23 @@ pub struct ConfigureLocalKmsRequest {
|
|||||||
pub cache_ttl_seconds: Option<u64>,
|
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
|
/// Request to configure KMS with Vault KV v2 + Transit backend
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConfigureVaultKmsRequest {
|
pub struct ConfigureVaultKmsRequest {
|
||||||
@@ -428,6 +447,7 @@ impl ConfigureKmsRequest {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::config::REDACTED_SECRET;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() {
|
fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() {
|
||||||
@@ -536,6 +556,104 @@ mod tests {
|
|||||||
other => panic!("expected vault-transit summary, got {other:?}"),
|
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
|
//! KMS configuration management
|
||||||
|
|
||||||
use crate::error::{KmsError, Result};
|
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 rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
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
|
/// KMS backend types
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum KmsBackend {
|
pub enum KmsBackend {
|
||||||
@@ -69,7 +118,7 @@ impl Default for KmsConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Backend-specific configuration
|
/// Backend-specific configuration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub enum BackendConfig {
|
pub enum BackendConfig {
|
||||||
/// Local backend configuration
|
/// Local backend configuration
|
||||||
Local(LocalConfig),
|
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
|
/// Local KMS backend configuration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct LocalConfig {
|
pub struct LocalConfig {
|
||||||
/// Directory to store key files
|
/// Directory to store key files
|
||||||
pub key_dir: PathBuf,
|
pub key_dir: PathBuf,
|
||||||
@@ -97,6 +156,17 @@ pub struct LocalConfig {
|
|||||||
pub file_permissions: Option<u32>,
|
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 {
|
impl Default for LocalConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -108,7 +178,7 @@ impl Default for LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit)
|
/// 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 {
|
pub struct VaultConfig {
|
||||||
/// Vault server URL
|
/// Vault server URL
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@@ -126,6 +196,20 @@ pub struct VaultConfig {
|
|||||||
pub tls: Option<TlsConfig>,
|
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 {
|
impl Default for VaultConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -143,7 +227,7 @@ impl Default for VaultConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Vault Transit backend configuration
|
/// Vault Transit backend configuration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct VaultTransitConfig {
|
pub struct VaultTransitConfig {
|
||||||
/// Vault server URL
|
/// Vault server URL
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@@ -157,6 +241,18 @@ pub struct VaultTransitConfig {
|
|||||||
pub tls: Option<TlsConfig>,
|
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 {
|
impl Default for VaultTransitConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -172,7 +268,7 @@ impl Default for VaultTransitConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Vault authentication methods
|
/// Vault authentication methods
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub enum VaultAuthMethod {
|
pub enum VaultAuthMethod {
|
||||||
/// Token authentication
|
/// Token authentication
|
||||||
Token { token: String },
|
Token { token: String },
|
||||||
@@ -180,6 +276,19 @@ pub enum VaultAuthMethod {
|
|||||||
AppRole { role_id: String, secret_id: String },
|
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
|
/// TLS configuration for Vault
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TlsConfig {
|
pub struct TlsConfig {
|
||||||
@@ -460,6 +569,7 @@ impl KmsConfig {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use rustfs_security_governance::validate_redaction_rules;
|
||||||
use temp_env::with_vars;
|
use temp_env::with_vars;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -551,6 +661,53 @@ mod tests {
|
|||||||
assert_eq!(serialized, "\"VaultTransit\"");
|
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]
|
#[test]
|
||||||
fn test_config_validation() {
|
fn test_config_validation() {
|
||||||
let mut config = KmsConfig::default();
|
let mut config = KmsConfig::default();
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
|||||||
## Current Context
|
## Current Context
|
||||||
|
|
||||||
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
|
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
|
||||||
- Branch: `overtrue/arch-kms-handler-actions`
|
- Branch: `overtrue/arch-kms-redaction`
|
||||||
- Baseline: `upstream/main` at `4fec606dc4f92b19e085f1609a188e82a72720ff`
|
- Baseline: `upstream/main` at `0cdcd1eb7bfd5fc229eb45f851c624084b072365`
|
||||||
- PR type for this branch: `security-change`
|
- PR type for this branch: `security-change`
|
||||||
- Runtime behavior changes: high-risk KMS admin handlers now authorize through
|
- Runtime behavior changes: KMS secret-bearing `Debug` output and admin status
|
||||||
dedicated `kms:*` actions instead of broad `ServerInfoAdminAction`.
|
summary views no longer expose local master keys, Vault tokens, or AppRole
|
||||||
- Rust code changes: migrate KMS handler action lists and route policy inventory
|
secret IDs. KMS backend behavior, authorization, production defaults, and
|
||||||
to dedicated KMS actions, while keeping temporary legacy create/status admin
|
config persistence are unchanged.
|
||||||
action compatibility with `RUSTFS_COMPAT_TODO(S-012)`.
|
- Rust code changes: add KMS redaction rules, safe `Debug` implementations for
|
||||||
|
secret-bearing KMS config and configure request types, and focused tests that
|
||||||
|
prove secrets are absent from debug/admin views while serde persistence keeps
|
||||||
|
the original values.
|
||||||
- CI/script changes: none
|
- CI/script changes: none
|
||||||
- Docs changes: record S-012 action migration status and temporary compatibility cleanup.
|
- Docs changes: record S-013 redaction status and the no-behavior-drift
|
||||||
|
migration boundary.
|
||||||
|
|
||||||
## Phase 0 Tasks
|
## Phase 0 Tasks
|
||||||
|
|
||||||
@@ -108,50 +112,63 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
|||||||
[`compat-cleanup-register.md`](compat-cleanup-register.md).
|
[`compat-cleanup-register.md`](compat-cleanup-register.md).
|
||||||
- Verification: focused handler and route policy tests, migration rules,
|
- Verification: focused handler and route policy tests, migration rules,
|
||||||
formatting, and `make pre-commit`.
|
formatting, and `make pre-commit`.
|
||||||
|
- [x] `S-013` Apply KMS redaction.
|
||||||
|
- Acceptance: KMS Debug output and admin status response summaries contain no
|
||||||
|
Vault token, AppRole secret ID, or local master key values.
|
||||||
|
- Must preserve: internal KMS config values remain available to runtime code
|
||||||
|
and persisted config serialization still writes the original secret values.
|
||||||
|
- Verification: focused KMS redaction/status tests, full KMS tests, migration
|
||||||
|
guards, Rust quality scan, clippy, and `make pre-commit` passed.
|
||||||
|
|
||||||
## Next PRs
|
## Next PRs
|
||||||
|
|
||||||
1. `contract`: add initial policy inventory tables for redaction, serde, or
|
1. `security-change`: inventory KMS development defaults before any production
|
||||||
supply-chain governance only after the contract shape remains stable.
|
default hardening.
|
||||||
2. `security-change`: apply KMS response/config redaction after action
|
2. `security-change`: apply IAM and plugin redaction in a separate S-014 PR.
|
||||||
migration settles.
|
|
||||||
|
|
||||||
## Pre-Push Review Log
|
## Pre-Push Review Log
|
||||||
|
|
||||||
| Expert | Status | Notes |
|
| Expert | Status | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Quality/architecture | pass | Single `security-change` PR; KMS auth action lists are local helper functions, names match `KmsAction`, and no storage/startup/global-state logic is touched. |
|
| Quality/architecture | pass | Single `security-change` PR; redaction rules use the security-governance crate, custom `Debug` stays local to secret-bearing KMS types, and no startup/storage/global-state path is touched. |
|
||||||
| Migration preservation | pass | Legacy create/status admin actions are retained only behind `RUSTFS_COMPAT_TODO(S-012)` and registered for cleanup; broad `ServerInfoAdminAction` is intentionally not retained for high-risk KMS operations. |
|
| Migration preservation | pass | Runtime secret access and persisted config serialization are explicitly preserved by tests; no temporary compatibility path is introduced. |
|
||||||
| Testing/verification | pass | Focused handler/route-policy tests cover dedicated actions and broad-action negative cases; migration rules, formatting, full `make pre-commit`, nextest, and doctests pass. |
|
| Testing/verification | pass | Focused redaction/status tests, full KMS tests, admin KMS handler tests, governance tests, clippy, migration guards, Rust quality scan, nextest, doctests, and `make pre-commit` passed. |
|
||||||
|
|
||||||
## Verification Notes
|
## Verification Notes
|
||||||
|
|
||||||
Passed:
|
Passed:
|
||||||
- Baseline `cargo test -p rustfs admin::handlers::kms --no-fail-fast`
|
- `cargo test -p rustfs-kms redaction -- --nocapture`
|
||||||
- Baseline `cargo test -p rustfs admin::route_policy --no-fail-fast`
|
- `cargo test -p rustfs-kms status_response -- --nocapture`
|
||||||
- `cargo fmt --all --check`
|
- `cargo test -p rustfs-kms --no-fail-fast`
|
||||||
- `cargo test -p rustfs admin::handlers::kms --no-fail-fast`
|
- `cargo test -p rustfs admin::handlers::kms --no-fail-fast`
|
||||||
- `cargo test -p rustfs admin::route_policy --no-fail-fast`
|
- `cargo test -p rustfs-security-governance --no-fail-fast`
|
||||||
|
- `cargo clippy -p rustfs-kms --all-targets --all-features -- -D warnings`
|
||||||
|
- Rust code quality scan on changed KMS source files
|
||||||
|
- `cargo fmt --all --check`
|
||||||
|
- `./scripts/check_layer_dependencies.sh`
|
||||||
- `./scripts/check_architecture_migration_rules.sh`
|
- `./scripts/check_architecture_migration_rules.sh`
|
||||||
|
- `./scripts/check_metrics_migration_refs.sh`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
- `make pre-commit`
|
- `make pre-commit`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- This branch changes only KMS admin authorization action selection and route
|
- This branch changes only KMS redaction for debug/admin view surfaces. It does
|
||||||
policy inventory. It does not change KMS runtime defaults, redaction, startup
|
not change KMS authorization, production defaults, startup order, global
|
||||||
order, global state, storage paths, or crate boundaries.
|
state, storage paths, route registration, or crate boundaries.
|
||||||
- `make pre-commit` passed all checks, including 5682 nextest tests and
|
- Config serialization still preserves secret values for persisted cluster
|
||||||
workspace doctests.
|
config; this is tested explicitly to avoid runtime data loss.
|
||||||
|
- `make pre-commit` passed all checks, including 5691 nextest tests, 111
|
||||||
|
skipped tests, and workspace doctests.
|
||||||
|
|
||||||
## Handoff Notes
|
## Handoff Notes
|
||||||
|
|
||||||
- Keep this S-012 branch as a focused `security-change` PR. Do not change KMS
|
- Keep this S-013 branch as a focused `security-change` PR. Do not change KMS
|
||||||
defaults, redaction, admin route registration shape, Config moves, Storage API
|
defaults, admin authorization, admin route registration shape, Config moves,
|
||||||
moves, Runtime moves, or ECStore moves.
|
Storage API moves, Runtime moves, or ECStore moves.
|
||||||
- `rustfs` may depend on `rustfs-security-governance` for contract metadata;
|
- `rustfs` may depend on `rustfs-security-governance` for contract metadata;
|
||||||
the security-governance crate must stay independent from implementation
|
the security-governance crate must stay independent from implementation
|
||||||
crates and runtime state.
|
crates and runtime state.
|
||||||
- Do not add temporary compatibility code without a matching
|
- Do not add temporary compatibility code without a matching
|
||||||
`RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry.
|
`RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry.
|
||||||
- The next KMS security PR should handle redaction or production default
|
- KMS production default hardening remains a separate task group; do not bundle
|
||||||
hardening separately; do not bundle those with this action migration.
|
it with this redaction PR.
|
||||||
|
|||||||
Reference in New Issue
Block a user