Files
rustfs/crates/kms/src/config.rs
T
唐小鸭 2216f00cfd fix(kms): unify persisted SSE data key envelopes (#5343)
* feat(kms): implement secure handling of static KMS secret keys and enhance encryption context validation

* feat: enhance local SSE DEK handling with JSON envelope format and versioning
2026-07-28 17:02:18 +08:00

1238 lines
47 KiB
Rust

// 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.
//! 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::{Path, PathBuf};
use std::time::Duration;
use url::Url;
pub const ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS: &str = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS";
pub const ENV_KMS_VAULT_SKIP_TLS_VERIFY: &str = "RUSTFS_KMS_VAULT_SKIP_TLS_VERIFY";
pub const ENV_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT: &str = "RUSTFS_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT";
pub const ENV_KMS_VAULT_TRANSIT_METADATA_PREFIX: &str = "RUSTFS_KMS_VAULT_TRANSIT_METADATA_PREFIX";
pub const ENV_KMS_STATIC_SECRET_KEY: &str = "RUSTFS_KMS_STATIC_SECRET_KEY";
pub const ENV_KMS_STATIC_SECRET_KEY_FILE: &str = "RUSTFS_KMS_STATIC_SECRET_KEY_FILE";
pub const DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT: &str = "secret";
pub const DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX: &str = "rustfs/kms/transit-metadata";
fn default_vault_transit_metadata_kv_mount() -> String {
DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT.to_string()
}
fn default_vault_transit_metadata_key_prefix() -> String {
DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX.to_string()
}
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",
),
RedactionRule::new("kms.static.secret_key", RedactionLevel::Secret, "static backend secret key material"),
];
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 {
/// 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,
/// Static single-key backend that derives DEKs from a pre-configured key
#[serde(rename = "Static")]
Static,
}
/// Main KMS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmsConfig {
/// Backend type
pub backend: KmsBackend,
/// Default master key ID for auto-encryption
pub default_key_id: Option<String>,
/// Backend-specific configuration
pub backend_config: BackendConfig,
/// Allow development-only insecure defaults such as plaintext local keys or HTTP Vault.
#[serde(default)]
pub allow_insecure_dev_defaults: bool,
/// Operation timeout
pub timeout: Duration,
/// Number of retry attempts
pub retry_attempts: u32,
/// Enable caching
pub enable_cache: bool,
/// Cache configuration
pub cache_config: CacheConfig,
}
impl Default for KmsConfig {
fn default() -> Self {
Self {
backend: KmsBackend::default(),
default_key_id: None,
backend_config: BackendConfig::default(),
allow_insecure_dev_defaults: false,
timeout: Duration::from_secs(30),
retry_attempts: 3,
enable_cache: true,
cache_config: CacheConfig::default(),
}
}
}
/// Backend-specific configuration
#[derive(Clone, Serialize, Deserialize)]
pub enum BackendConfig {
/// Local backend configuration
Local(LocalConfig),
/// Vault KV v2 + Transit backend configuration
#[serde(rename = "VaultKV2", alias = "Vault")]
VaultKv2(Box<VaultConfig>),
/// Vault Transit backend configuration
VaultTransit(Box<VaultTransitConfig>),
/// Static single-key backend configuration
Static(StaticConfig),
}
impl Default for BackendConfig {
fn default() -> Self {
Self::Local(LocalConfig::default())
}
}
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(),
Self::Static(config) => f.debug_tuple("Static").field(config).finish(),
}
}
}
/// Local KMS backend configuration
#[derive(Clone, Serialize, Deserialize)]
pub struct LocalConfig {
/// Directory to store key files
pub key_dir: PathBuf,
/// Master key for encrypting stored keys (if None, only explicit development-mode plaintext storage is allowed)
pub master_key: Option<String>,
/// File permissions for key files (octal)
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 {
key_dir: std::env::temp_dir().join("rustfs_kms_keys"),
master_key: None,
file_permissions: Some(0o600), // Owner read/write only
}
}
}
/// Static single-key KMS backend configuration
///
/// Uses a pre-configured AES-256 key to derive data encryption keys via
/// HMAC-SHA256 + AES-256-GCM, matching the MinIO builtin/static KMS wire format.
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct StaticConfig {
/// Key identifier (name) for the single configured key
pub key_id: String,
/// Base64-encoded 32-byte AES-256 key material (zeroed on drop)
#[serde(skip_serializing, default)]
pub secret_key: String,
}
impl Drop for StaticConfig {
fn drop(&mut self) {
use zeroize::Zeroize;
self.secret_key.zeroize();
}
}
impl fmt::Debug for StaticConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StaticConfig")
.field("key_id", &self.key_id)
.field("secret_key", &redacted_secret(&self.secret_key))
.finish()
}
}
impl StaticConfig {
/// Decode the base64-encoded secret key into raw bytes.
/// Returns an error if the key is not valid base64 or is not exactly 32 bytes.
pub fn decode_key(&self) -> Result<[u8; 32]> {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&self.secret_key)
.map_err(|e| KmsError::configuration_error(format!("Static KMS secret key is not valid base64: {e}")))?;
if bytes.len() != 32 {
return Err(KmsError::configuration_error(format!(
"Static KMS secret key must be exactly 32 bytes after base64 decoding, got {} bytes",
bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(key)
}
}
/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit)
#[derive(Clone, Serialize, Deserialize)]
pub struct VaultConfig {
/// 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,
/// KV engine mount path for storing keys
pub kv_mount: String,
/// Path prefix for keys in KV store
pub key_path_prefix: String,
/// TLS configuration
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 {
address: "http://localhost:8200".to_string(),
auth_method: VaultAuthMethod::Token {
token: "dev-token".to_string(),
},
namespace: None,
mount_path: "transit".to_string(),
kv_mount: "secret".to_string(),
key_path_prefix: "rustfs/kms/keys".to_string(),
tls: None,
}
}
}
/// Vault Transit backend configuration
#[derive(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,
/// KV v2 mount path for persisting transit key metadata
#[serde(default = "default_vault_transit_metadata_kv_mount")]
pub metadata_kv_mount: String,
/// Key path prefix under metadata_kv_mount for transit key metadata storage
#[serde(default = "default_vault_transit_metadata_key_prefix")]
pub metadata_key_prefix: String,
/// TLS configuration
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("metadata_kv_mount", &self.metadata_kv_mount)
.field("metadata_key_prefix", &self.metadata_key_prefix)
.field("tls", &self.tls)
.finish()
}
}
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(),
metadata_kv_mount: default_vault_transit_metadata_kv_mount(),
metadata_key_prefix: default_vault_transit_metadata_key_prefix(),
tls: None,
}
}
}
/// Vault authentication methods
#[derive(Clone, Serialize, Deserialize)]
pub enum VaultAuthMethod {
/// Token authentication
Token { token: String },
/// AppRole authentication
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 {
/// Path to CA certificate file
pub ca_cert_path: Option<PathBuf>,
/// Path to client certificate file
pub client_cert_path: Option<PathBuf>,
/// Path to client private key file
pub client_key_path: Option<PathBuf>,
/// Skip TLS verification (insecure, for development only)
pub skip_verify: bool,
}
/// Cache configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
/// Maximum number of keys to cache
pub max_keys: usize,
/// TTL for cached keys
pub ttl: Duration,
/// Enable cache metrics
pub enable_metrics: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_keys: 1000,
ttl: Duration::from_secs(3600), // 1 hour
enable_metrics: true,
}
}
}
impl KmsConfig {
/// Create a new KMS configuration for local backend (for development and testing only)
pub fn local(key_dir: PathBuf) -> Self {
Self {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir,
..Default::default()
}),
..Default::default()
}
}
/// 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::VaultKv2,
backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig {
address: address.to_string(),
auth_method: VaultAuthMethod::Token { token },
..Default::default()
})),
..Default::default()
}
}
/// 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::VaultKv2,
backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig {
address: address.to_string(),
auth_method: VaultAuthMethod::AppRole { role_id, secret_id },
..Default::default()
})),
..Default::default()
}
}
/// 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()
}
}
/// Create a new KMS configuration for static single-key backend
///
/// # Arguments
/// * `key_id` - The key identifier (name) for the configured key
/// * `secret_key` - Base64-encoded 32-byte AES-256 key material
pub fn static_kms(key_id: String, secret_key: String) -> Self {
Self {
backend: KmsBackend::Static,
backend_config: BackendConfig::Static(StaticConfig {
key_id: key_id.clone(),
secret_key,
}),
default_key_id: Some(key_id),
..Default::default()
}
}
/// Get the local configuration if backend is Local
pub fn local_config(&self) -> Option<&LocalConfig> {
match &self.backend_config {
BackendConfig::Local(config) => Some(config),
_ => None,
}
}
/// Get the Vault KV2 configuration if backend is VaultKv2
pub fn vault_config(&self) -> Option<&VaultConfig> {
match &self.backend_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,
}
}
/// Get the static configuration if backend is Static
pub fn static_config(&self) -> Option<&StaticConfig> {
match &self.backend_config {
BackendConfig::Static(config) => Some(config),
_ => None,
}
}
/// Set default key ID
pub fn with_default_key(mut self, key_id: String) -> Self {
self.default_key_id = Some(key_id);
self
}
/// Explicitly allow development-only KMS defaults.
pub fn with_insecure_development_defaults(mut self) -> Self {
self.allow_insecure_dev_defaults = true;
self
}
/// Set operation timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Enable or disable caching
pub fn with_cache(mut self, enable: bool) -> Self {
self.enable_cache = enable;
self
}
/// Validate the configuration
pub fn validate(&self) -> Result<()> {
// Validate timeout
if self.timeout.is_zero() {
return Err(KmsError::configuration_error("Timeout must be greater than 0"));
}
// Validate retry attempts
if self.retry_attempts == 0 {
return Err(KmsError::configuration_error("Retry attempts must be greater than 0"));
}
// Validate backend-specific configuration
match &self.backend_config {
BackendConfig::Local(config) => {
if !config.key_dir.is_absolute() {
return Err(KmsError::configuration_error("Local key directory must be an absolute path"));
}
if !self.allow_insecure_dev_defaults {
if config.master_key.as_deref().is_none_or(str::is_empty) {
return Err(development_default_error(
"Local KMS requires a master key outside explicit development mode",
));
}
if is_under_temp_dir(&config.key_dir) {
return Err(development_default_error(
"Local KMS key directory must not be under the process temp directory outside explicit development mode",
));
}
}
}
BackendConfig::VaultKv2(config) => {
if !config.address.starts_with("http://") && !config.address.starts_with("https://") {
return Err(KmsError::configuration_error("Vault KV2 address must use http or https scheme"));
}
if !self.allow_insecure_dev_defaults {
validate_vault_development_defaults("Vault KV2", &config.address, &config.auth_method, config.tls.as_ref())?;
}
if config.mount_path.is_empty() {
return Err(KmsError::configuration_error("Vault KV2 mount path cannot be empty"));
}
// Validate TLS configuration if using HTTPS
if config.address.starts_with("https://")
&& let Some(ref tls) = config.tls
&& !tls.skip_verify
{
// In production, we should have proper TLS configuration
if tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() {
tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA");
}
}
}
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 !self.allow_insecure_dev_defaults {
validate_vault_development_defaults(
"Vault Transit",
&config.address,
&config.auth_method,
config.tls.as_ref(),
)?;
}
if config.mount_path.is_empty() {
return Err(KmsError::configuration_error("Vault Transit mount path cannot be empty"));
}
if config.metadata_kv_mount.is_empty() {
return Err(KmsError::configuration_error("Vault Transit metadata KV mount cannot be empty"));
}
if config.metadata_key_prefix.is_empty() {
return Err(KmsError::configuration_error("Vault Transit metadata key prefix 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");
}
}
BackendConfig::Static(config) => {
if config.key_id.is_empty() {
return Err(KmsError::configuration_error("Static KMS key_id cannot be empty"));
}
if config.secret_key.is_empty() {
return Err(KmsError::configuration_error("Static KMS secret_key cannot be empty"));
}
// Validate that the key can be decoded (right length, valid base64)
config.decode_key()?;
}
}
// Validate cache configuration
if self.enable_cache && self.cache_config.max_keys == 0 {
return Err(KmsError::configuration_error("Cache max_keys must be greater than 0"));
}
Ok(())
}
/// Load configuration from environment variables
pub fn from_env() -> Result<Self> {
let mut config = Self::default();
// Backend type
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" | "vault-kv2" | "vault_kv2" => KmsBackend::VaultKv2,
"vault-transit" | "vault_transit" => KmsBackend::VaultTransit,
"static" => KmsBackend::Static,
_ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {backend_type}"))),
};
}
// Default key ID
if let Some(key_id) = get_env_opt_str("RUSTFS_KMS_DEFAULT_KEY_ID") {
config.default_key_id = Some(key_id);
}
// Timeout
if let Some(timeout_str) = get_env_opt_str("RUSTFS_KMS_TIMEOUT_SECS") {
let timeout_secs = timeout_str
.parse::<u64>()
.map_err(|_| KmsError::configuration_error("Invalid timeout value"))?;
config.timeout = Duration::from_secs(timeout_secs);
}
// Retry attempts
if let Some(retries_str) = get_env_opt_str("RUSTFS_KMS_RETRY_ATTEMPTS") {
config.retry_attempts = retries_str
.parse()
.map_err(|_| KmsError::configuration_error("Invalid retry attempts value"))?;
}
// Enable cache
config.enable_cache = get_env_bool("RUSTFS_KMS_ENABLE_CACHE", config.enable_cache);
config.allow_insecure_dev_defaults =
get_env_bool(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, config.allow_insecure_dev_defaults);
// Backend-specific configuration
match config.backend {
KmsBackend::Local => {
let key_dir = get_env_str("RUSTFS_KMS_LOCAL_KEY_DIR", "./kms_keys");
let master_key = get_env_opt_str("RUSTFS_KMS_LOCAL_MASTER_KEY");
config.backend_config = BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from(key_dir),
master_key,
file_permissions: Some(0o600),
});
}
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");
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig {
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"),
kv_mount: get_env_str("RUSTFS_KMS_VAULT_KV_MOUNT", "secret"),
key_path_prefix: get_env_str("RUSTFS_KMS_VAULT_KEY_PREFIX", "rustfs/kms/keys"),
tls: vault_tls_config(skip_tls_verify),
}));
}
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");
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
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"),
metadata_kv_mount: get_env_str(
ENV_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT,
DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT,
),
metadata_key_prefix: get_env_str(
ENV_KMS_VAULT_TRANSIT_METADATA_PREFIX,
DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX,
),
tls: vault_tls_config(skip_tls_verify),
}));
}
KmsBackend::Static => {
// Read from file first, then fall back to direct env var
let secret_str = if let Some(file_path) = get_env_opt_str(ENV_KMS_STATIC_SECRET_KEY_FILE) {
std::fs::read_to_string(&file_path).map_err(|e| {
KmsError::configuration_error(format!("Failed to read static KMS secret key file {file_path}: {e}"))
})?
} else {
get_env_str(ENV_KMS_STATIC_SECRET_KEY, "")
};
let secret_str = secret_str.trim().to_string();
if secret_str.is_empty() {
return Err(KmsError::configuration_error(format!(
"Static KMS requires {ENV_KMS_STATIC_SECRET_KEY} or {ENV_KMS_STATIC_SECRET_KEY_FILE} to be set"
)));
}
// Parse format: <key-id>:<base64-key>
let colon_pos = secret_str.find(':').ok_or_else(|| {
KmsError::configuration_error("Static KMS secret key must be in format <key-name>:<base64-key>")
})?;
let key_id = secret_str[..colon_pos].to_string();
let secret_key = secret_str[colon_pos + 1..].to_string();
if key_id.is_empty() {
return Err(KmsError::configuration_error(
"Static KMS key name must not be empty in secret key string",
));
}
if secret_key.is_empty() {
return Err(KmsError::configuration_error(
"Static KMS base64 key must not be empty in secret key string",
));
}
config.backend_config = BackendConfig::Static(StaticConfig {
key_id: key_id.clone(),
secret_key,
});
config.default_key_id = Some(key_id);
}
}
config.validate()?;
Ok(config)
}
}
fn vault_tls_config(skip_tls_verify: bool) -> Option<TlsConfig> {
skip_tls_verify.then_some(TlsConfig {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
skip_verify: true,
})
}
fn development_default_error(reason: &str) -> KmsError {
KmsError::configuration_error(format!("{reason}; set {ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS}=true only for development"))
}
fn is_under_temp_dir(path: &Path) -> bool {
path.starts_with(std::env::temp_dir())
}
fn validate_vault_development_defaults(
backend_name: &str,
address: &str,
auth_method: &VaultAuthMethod,
tls: Option<&TlsConfig>,
) -> Result<()> {
if address.starts_with("http://") {
return Err(development_default_error(&format!(
"{backend_name} requires HTTPS outside explicit development mode"
)));
}
if matches!(auth_method, VaultAuthMethod::Token { token } if token == "dev-token") {
return Err(development_default_error(&format!(
"{backend_name} default dev-token is not allowed outside explicit development mode"
)));
}
if tls.is_some_and(|tls| tls.skip_verify) {
return Err(development_default_error(&format!(
"{backend_name} skip TLS verification is not allowed outside explicit development mode"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rustfs_security_governance::validate_redaction_rules;
use temp_env::with_vars;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = KmsConfig::default();
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_err());
assert!(config.with_insecure_development_defaults().validate().is_ok());
}
#[test]
fn test_local_config() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults();
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_ok());
let local_config = config.local_config().expect("Should have local config");
assert_eq!(local_config.key_dir, temp_dir.path());
}
#[test]
fn test_local_development_defaults_require_opt_in() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
assert!(config.validate().is_err());
assert!(config.with_insecure_development_defaults().validate().is_ok());
let production_config = KmsConfig {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from("/var/lib/rustfs/kms"),
master_key: Some("production-master-key".to_string()),
file_permissions: Some(0o600),
}),
..Default::default()
};
assert!(production_config.validate().is_ok());
}
#[test]
fn test_vault_config() {
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::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_development_defaults_require_opt_in() {
let http_address = Url::parse("http://127.0.0.1:8200").expect("Valid URL");
let https_address = Url::parse("https://vault.example.com:8200").expect("Valid URL");
let http_config = KmsConfig::vault(http_address, "vault-token".to_string());
assert!(http_config.validate().is_err());
assert!(http_config.with_insecure_development_defaults().validate().is_ok());
let dev_token_config = KmsConfig::vault(https_address.clone(), "dev-token".to_string());
assert!(dev_token_config.validate().is_err());
assert!(dev_token_config.with_insecure_development_defaults().validate().is_ok());
let skip_tls_config = KmsConfig {
backend: KmsBackend::VaultTransit,
backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig {
address: https_address.to_string(),
auth_method: VaultAuthMethod::Token {
token: "vault-token".to_string(),
},
namespace: None,
mount_path: "transit".to_string(),
metadata_kv_mount: DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT.to_string(),
metadata_key_prefix: DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX.to_string(),
tls: Some(TlsConfig {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
skip_verify: true,
}),
})),
..Default::default()
};
assert!(skip_tls_config.validate().is_err());
assert!(skip_tls_config.with_insecure_development_defaults().validate().is_ok());
}
#[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_legacy_persisted_vault_transit_config_uses_metadata_defaults() {
let raw = r#"{
"backend": "VaultTransit",
"backend_config": {
"VaultTransit": {
"address": "http://127.0.0.1:8200",
"auth_method": { "Token": { "token": "t" } },
"namespace": null,
"mount_path": "transit",
"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 vault-transit config");
assert_eq!(config.backend, KmsBackend::VaultTransit);
let vault = config
.vault_transit_config()
.expect("vault transit config should deserialize");
assert_eq!(vault.metadata_kv_mount, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT);
assert_eq!(vault.metadata_key_prefix, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX);
}
#[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_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 static_kms_config_serialization_does_not_expose_key_material() {
use base64::Engine as _;
let encoded_key = base64::engine::general_purpose::STANDARD.encode([0x5au8; 32]);
let config = KmsConfig::static_kms("static-key".to_string(), encoded_key.clone());
let serialized = serde_json::to_string(&config).expect("static KMS config should serialize");
assert!(
!serialized.contains(&encoded_key),
"persisted static KMS configuration must not contain plaintext key material"
);
}
#[test]
fn test_config_validation() {
let mut config = KmsConfig {
allow_insecure_dev_defaults: true,
..Default::default()
};
// Valid config
assert!(config.validate().is_ok());
// Invalid timeout
config.timeout = Duration::from_secs(0);
assert!(config.validate().is_err());
// Reset timeout and test invalid retry attempts
config.timeout = Duration::from_secs(30);
config.retry_attempts = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_from_env_reads_vault_settings() {
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault")),
("RUSTFS_KMS_DEFAULT_KEY_ID", Some("tenant-key")),
("RUSTFS_KMS_TIMEOUT_SECS", Some("42")),
("RUSTFS_KMS_RETRY_ATTEMPTS", Some("7")),
("RUSTFS_KMS_ENABLE_CACHE", Some("false")),
("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")),
("RUSTFS_KMS_VAULT_KV_MOUNT", Some("secret-alt")),
("RUSTFS_KMS_VAULT_KEY_PREFIX", Some("tenant/keys")),
],
|| {
let config = KmsConfig::from_env().expect("kms config should load from env");
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);
assert!(!config.enable_cache);
let vault = config.vault_config().expect("vault 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");
assert_eq!(vault.kv_mount, "secret-alt");
assert_eq!(vault.key_path_prefix, "tenant/keys");
},
);
}
#[test]
fn test_from_env_requires_vault_development_opt_in() {
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")),
("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")),
],
|| {
let error = KmsConfig::from_env().expect_err("vault dev defaults should fail closed");
assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS));
},
);
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")),
("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")),
(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, Some("true")),
],
|| {
let config = KmsConfig::from_env().expect("explicit development opt-in should allow vault dev defaults");
assert!(config.allow_insecure_dev_defaults);
},
);
}
#[test]
fn test_from_env_rejects_vault_skip_tls_verify_without_opt_in() {
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault-transit")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")),
("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")),
(ENV_KMS_VAULT_SKIP_TLS_VERIFY, Some("true")),
],
|| {
let error = KmsConfig::from_env().expect_err("skip TLS verify should fail closed");
assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS));
},
);
}
#[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");
},
);
}
#[test]
fn test_from_env_reads_static_secret_file_and_sets_default_key() {
use base64::Engine as _;
let temp_dir = TempDir::new().expect("create temp dir for static KMS secret");
let secret_path = temp_dir.path().join("static-kms-secret");
// Named `*_key_b64` (not `*_secret`) so the logging-guardrails check does not
// flag these fixture interpolations as secrets leaking into log strings.
let file_key_b64 = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
let env_key_b64 = base64::engine::general_purpose::STANDARD.encode([9u8; 32]);
std::fs::write(&secret_path, format!("file-key:{file_key_b64}\n")).expect("write static KMS secret file");
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("static")),
(
ENV_KMS_STATIC_SECRET_KEY_FILE,
Some(secret_path.to_str().expect("secret path should be utf-8")),
),
(ENV_KMS_STATIC_SECRET_KEY, Some(&format!("env-key:{env_key_b64}"))),
],
|| {
let config = KmsConfig::from_env().expect("static KMS config should load from secret file");
assert_eq!(config.backend, KmsBackend::Static);
assert_eq!(config.default_key_id.as_deref(), Some("file-key"));
let static_config = config.static_config().expect("static backend config");
assert_eq!(static_config.key_id, "file-key");
assert_eq!(static_config.secret_key, file_key_b64);
},
);
}
}