feat(kms): add static single-key backend (#5222)

This commit is contained in:
唐小鸭
2026-07-26 04:12:56 +08:00
committed by GitHub
parent c984bc7251
commit 99e1f5fbd2
14 changed files with 972 additions and 22 deletions
+127
View File
@@ -27,6 +27,8 @@ pub const ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS: &str = "RUSTFS_KMS_ALLOW_INSECURE
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";
@@ -73,6 +75,7 @@ pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
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***";
@@ -97,6 +100,9 @@ pub enum KmsBackend {
/// 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
@@ -146,6 +152,8 @@ pub enum BackendConfig {
VaultKv2(Box<VaultConfig>),
/// Vault Transit backend configuration
VaultTransit(Box<VaultTransitConfig>),
/// Static single-key backend configuration
Static(StaticConfig),
}
impl Default for BackendConfig {
@@ -160,6 +168,7 @@ impl fmt::Debug for BackendConfig {
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(),
}
}
}
@@ -196,6 +205,47 @@ impl Default for LocalConfig {
}
}
/// 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)
pub secret_key: String,
}
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 {
@@ -404,6 +454,23 @@ impl KmsConfig {
}
}
/// 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 {
@@ -428,6 +495,14 @@ impl KmsConfig {
}
}
/// 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);
@@ -544,6 +619,16 @@ impl KmsConfig {
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
@@ -564,6 +649,7 @@ impl KmsConfig {
"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}"))),
};
}
@@ -641,6 +727,47 @@ impl KmsConfig {
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()?;