mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
feat(kms): add static single-key backend (#5222)
This commit is contained in:
@@ -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()?;
|
||||
|
||||
Reference in New Issue
Block a user