mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(kms): add static single-key backend (#5222)
This commit is contained in:
Vendored
+10
-6
@@ -212,13 +212,17 @@
|
|||||||
// "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
// "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||||
|
|
||||||
// 4. kms vault transit backend test key
|
// 4. kms vault transit backend test key
|
||||||
"RUSTFS_KMS_ENABLE": "true",
|
// "RUSTFS_KMS_ENABLE": "true",
|
||||||
"RUSTFS_KMS_BACKEND": "vault-transit",
|
// "RUSTFS_KMS_BACKEND": "vault-transit",
|
||||||
"RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200",
|
// "RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200",
|
||||||
"RUSTFS_KMS_VAULT_TOKEN": "Dev Token",
|
// "RUSTFS_KMS_VAULT_TOKEN": "Dev Token",
|
||||||
"RUSTFS_KMS_VAULT_MOUNT_PATH": "transit",
|
// "RUSTFS_KMS_VAULT_MOUNT_PATH": "transit",
|
||||||
"RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
// "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key",
|
||||||
|
|
||||||
|
// 5、kms static backend test key
|
||||||
|
"RUSTFS_KMS_ENABLE": "true",
|
||||||
|
"RUSTFS_KMS_BACKEND": "static",
|
||||||
|
"RUSTFS_KMS_STATIC_SECRET_KEY": "rustfs-master-key:2dfNXGHlsEflGVCxb+5DIdGEl1sIvtwX+QfmYasi5QM="
|
||||||
},
|
},
|
||||||
"sourceLanguages": [
|
"sourceLanguages": [
|
||||||
"rust"
|
"rust"
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
BackendConfig, CacheConfig, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, KmsBackend,
|
BackendConfig, CacheConfig, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, KmsBackend,
|
||||||
KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig, redacted_secret_option,
|
KmsConfig, LocalConfig, StaticConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig, redacted_secret,
|
||||||
|
redacted_secret_option,
|
||||||
};
|
};
|
||||||
use crate::service_manager::KmsServiceStatus;
|
use crate::service_manager::KmsServiceStatus;
|
||||||
use crate::types::{KeyMetadata, KeyUsage};
|
use crate::types::{KeyMetadata, KeyUsage};
|
||||||
@@ -136,6 +137,46 @@ pub struct ConfigureVaultTransitKmsRequest {
|
|||||||
pub allow_insecure_dev_defaults: Option<bool>,
|
pub allow_insecure_dev_defaults: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request to configure KMS with Static single-key backend
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ConfigureStaticKmsRequest {
|
||||||
|
/// Key identifier (name) for the single configured key
|
||||||
|
pub key_id: String,
|
||||||
|
/// Base64-encoded 32-byte AES-256 key material
|
||||||
|
pub secret_key: String,
|
||||||
|
/// Default master key ID for auto-encryption
|
||||||
|
pub default_key_id: Option<String>,
|
||||||
|
/// Operation timeout in seconds
|
||||||
|
pub timeout_seconds: Option<u64>,
|
||||||
|
/// Number of retry attempts
|
||||||
|
pub retry_attempts: Option<u32>,
|
||||||
|
/// Enable caching
|
||||||
|
pub enable_cache: Option<bool>,
|
||||||
|
/// Maximum number of keys to cache
|
||||||
|
pub max_cached_keys: Option<usize>,
|
||||||
|
/// Cache TTL in seconds
|
||||||
|
pub cache_ttl_seconds: Option<u64>,
|
||||||
|
/// Allow development-only insecure defaults
|
||||||
|
pub allow_insecure_dev_defaults: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ConfigureStaticKmsRequest {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("ConfigureStaticKmsRequest")
|
||||||
|
.field("key_id", &self.key_id)
|
||||||
|
.field("secret_key", &redacted_secret(&self.secret_key))
|
||||||
|
.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)
|
||||||
|
.field("allow_insecure_dev_defaults", &self.allow_insecure_dev_defaults)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generic KMS configuration request
|
/// Generic KMS configuration request
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "backend_type")]
|
#[serde(tag = "backend_type")]
|
||||||
@@ -155,6 +196,9 @@ pub enum ConfigureKmsRequest {
|
|||||||
/// Configure with Vault Transit backend
|
/// Configure with Vault Transit backend
|
||||||
#[serde(rename = "VaultTransit", alias = "vault-transit", alias = "vault_transit")]
|
#[serde(rename = "VaultTransit", alias = "vault-transit", alias = "vault_transit")]
|
||||||
VaultTransit(ConfigureVaultTransitKmsRequest),
|
VaultTransit(ConfigureVaultTransitKmsRequest),
|
||||||
|
/// Configure with Static single-key backend
|
||||||
|
#[serde(rename = "Static", alias = "static")]
|
||||||
|
Static(ConfigureStaticKmsRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KMS configuration response
|
/// KMS configuration response
|
||||||
@@ -316,6 +360,11 @@ pub enum BackendSummary {
|
|||||||
/// Skip TLS verification
|
/// Skip TLS verification
|
||||||
skip_tls_verify: bool,
|
skip_tls_verify: bool,
|
||||||
},
|
},
|
||||||
|
/// Static single-key backend summary
|
||||||
|
Static {
|
||||||
|
/// Configured key identifier
|
||||||
|
key_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&KmsConfig> for KmsConfigSummary {
|
impl From<&KmsConfig> for KmsConfigSummary {
|
||||||
@@ -360,6 +409,9 @@ impl From<&KmsConfig> for KmsConfigSummary {
|
|||||||
mount_path: vault_config.mount_path.clone(),
|
mount_path: vault_config.mount_path.clone(),
|
||||||
skip_tls_verify: vault_config.tls.as_ref().is_some_and(|tls| tls.skip_verify),
|
skip_tls_verify: vault_config.tls.as_ref().is_some_and(|tls| tls.skip_verify),
|
||||||
},
|
},
|
||||||
|
BackendConfig::Static(static_config) => BackendSummary::Static {
|
||||||
|
key_id: static_config.key_id.clone(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -474,6 +526,29 @@ impl ConfigureVaultTransitKmsRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConfigureStaticKmsRequest {
|
||||||
|
/// Convert to KmsConfig
|
||||||
|
pub fn to_kms_config(&self) -> KmsConfig {
|
||||||
|
KmsConfig {
|
||||||
|
backend: KmsBackend::Static,
|
||||||
|
default_key_id: self.default_key_id.clone(),
|
||||||
|
backend_config: BackendConfig::Static(StaticConfig {
|
||||||
|
key_id: self.key_id.clone(),
|
||||||
|
secret_key: self.secret_key.clone(),
|
||||||
|
}),
|
||||||
|
allow_insecure_dev_defaults: self.allow_insecure_dev_defaults.unwrap_or(false),
|
||||||
|
timeout: Duration::from_secs(self.timeout_seconds.unwrap_or(30)),
|
||||||
|
retry_attempts: self.retry_attempts.unwrap_or(3),
|
||||||
|
enable_cache: self.enable_cache.unwrap_or(true),
|
||||||
|
cache_config: CacheConfig {
|
||||||
|
max_keys: self.max_cached_keys.unwrap_or(1000),
|
||||||
|
ttl: Duration::from_secs(self.cache_ttl_seconds.unwrap_or(3600)),
|
||||||
|
enable_metrics: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ConfigureKmsRequest {
|
impl ConfigureKmsRequest {
|
||||||
/// Convert to KmsConfig
|
/// Convert to KmsConfig
|
||||||
pub fn to_kms_config(&self) -> KmsConfig {
|
pub fn to_kms_config(&self) -> KmsConfig {
|
||||||
@@ -481,6 +556,7 @@ impl ConfigureKmsRequest {
|
|||||||
ConfigureKmsRequest::Local(req) => req.to_kms_config(),
|
ConfigureKmsRequest::Local(req) => req.to_kms_config(),
|
||||||
ConfigureKmsRequest::VaultKv2(req) => req.to_kms_config(),
|
ConfigureKmsRequest::VaultKv2(req) => req.to_kms_config(),
|
||||||
ConfigureKmsRequest::VaultTransit(req) => req.to_kms_config(),
|
ConfigureKmsRequest::VaultTransit(req) => req.to_kms_config(),
|
||||||
|
ConfigureKmsRequest::Static(req) => req.to_kms_config(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,7 +687,9 @@ impl LocalKmsBackend {
|
|||||||
|
|
||||||
let local_config = match &config.backend_config {
|
let local_config = match &config.backend_config {
|
||||||
crate::config::BackendConfig::Local(local_config) => local_config.clone(),
|
crate::config::BackendConfig::Local(local_config) => local_config.clone(),
|
||||||
crate::config::BackendConfig::VaultKv2(_) | crate::config::BackendConfig::VaultTransit(_) => {
|
crate::config::BackendConfig::VaultKv2(_)
|
||||||
|
| crate::config::BackendConfig::VaultTransit(_)
|
||||||
|
| crate::config::BackendConfig::Static(_) => {
|
||||||
return Err(KmsError::configuration_error("Expected Local backend configuration"));
|
return Err(KmsError::configuration_error("Expected Local backend configuration"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use async_trait::async_trait;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub mod local;
|
pub mod local;
|
||||||
|
pub mod static_kms;
|
||||||
pub mod vault;
|
pub mod vault;
|
||||||
pub mod vault_transit;
|
pub mod vault_transit;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,655 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//! Static single-key KMS backend implementation
|
||||||
|
//!
|
||||||
|
//! This backend holds one pre-configured AES-256 key and uses it directly
|
||||||
|
//! to encrypt/decrypt data encryption keys (DEKs) via AES-256-GCM.
|
||||||
|
//!
|
||||||
|
//! ## Ciphertext format
|
||||||
|
//!
|
||||||
|
//! encrypted_data(plaintext_len+16) || nonce (12 bytes)
|
||||||
|
|
||||||
|
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
|
||||||
|
use crate::config::{BackendConfig, KmsConfig};
|
||||||
|
use crate::error::{KmsError, Result};
|
||||||
|
use crate::types::*;
|
||||||
|
use aes_gcm::{
|
||||||
|
Aes256Gcm, Key, Nonce,
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use jiff::Zoned;
|
||||||
|
use rand::RngExt;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::debug;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
/// AES-GCM nonce size in bytes, appended to each ciphertext.
|
||||||
|
const NONCE_SIZE: usize = 12;
|
||||||
|
/// AES-256 key size in bytes.
|
||||||
|
const KEY_SIZE: usize = 32;
|
||||||
|
|
||||||
|
/// Static single-key KMS backend.
|
||||||
|
///
|
||||||
|
/// Uses a pre-configured AES-256 key to derive data encryption keys. This is a
|
||||||
|
/// read-only backend: it cannot create, delete, or rotate keys.
|
||||||
|
pub struct StaticKmsBackend {
|
||||||
|
/// The configured key identifier (name).
|
||||||
|
key_id: String,
|
||||||
|
/// The raw 32-byte AES-256 key material (zeroed on drop).
|
||||||
|
key: Zeroizing<[u8; KEY_SIZE]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticKmsBackend {
|
||||||
|
/// Create a new static KMS backend from configuration.
|
||||||
|
pub async fn new(mut config: KmsConfig) -> Result<Self> {
|
||||||
|
let BackendConfig::Static(ref mut static_config) = config.backend_config else {
|
||||||
|
return Err(KmsError::configuration_error("Static KMS backend requires StaticConfig"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = static_config.decode_key()?;
|
||||||
|
|
||||||
|
// Zeroize the base64-encoded secret key in the config after extracting the raw key.
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
static_config.secret_key.zeroize();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
key_id = %static_config.key_id,
|
||||||
|
"Static KMS backend initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
key_id: static_config.key_id.clone(),
|
||||||
|
key: key.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the key metadata for the single configured key.
|
||||||
|
fn key_metadata(&self) -> KeyMetadata {
|
||||||
|
KeyMetadata {
|
||||||
|
key_id: self.key_id.clone(),
|
||||||
|
key_state: KeyState::Enabled,
|
||||||
|
key_usage: KeyUsage::EncryptDecrypt,
|
||||||
|
description: Some("Static single-key KMS backend".to_string()),
|
||||||
|
creation_date: Zoned::now(),
|
||||||
|
deletion_date: None,
|
||||||
|
origin: "EXTERNAL".to_string(),
|
||||||
|
key_manager: "STATIC".to_string(),
|
||||||
|
tags: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl KmsClient for StaticKmsBackend {
|
||||||
|
async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
|
||||||
|
if request.master_key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(&request.master_key_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 12-byte random nonce
|
||||||
|
let mut nonce_bytes = [0u8; NONCE_SIZE];
|
||||||
|
rand::rng().fill(&mut nonce_bytes[..]);
|
||||||
|
|
||||||
|
// Generate 32 random bytes as plaintext DEK
|
||||||
|
let mut plaintext = [0u8; KEY_SIZE];
|
||||||
|
rand::rng().fill(&mut plaintext[..]);
|
||||||
|
|
||||||
|
// Encrypt DEK with AES-256-GCM using the static key directly
|
||||||
|
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||||
|
let cipher = Aes256Gcm::new(&key);
|
||||||
|
let nonce = Nonce::from(nonce_bytes);
|
||||||
|
|
||||||
|
let encrypted = cipher
|
||||||
|
.encrypt(&nonce, plaintext.as_ref())
|
||||||
|
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?;
|
||||||
|
|
||||||
|
// Ciphertext format: encrypted_dek || nonce
|
||||||
|
let mut ciphertext = encrypted;
|
||||||
|
ciphertext.extend_from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
Ok(DataKeyInfo::new(
|
||||||
|
self.key_id.clone(),
|
||||||
|
0, // version is always 0 for static KMS
|
||||||
|
Some(plaintext.to_vec()),
|
||||||
|
ciphertext,
|
||||||
|
"AES_256".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
|
||||||
|
if request.key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(&request.key_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 12-byte random nonce
|
||||||
|
let mut nonce_bytes = [0u8; NONCE_SIZE];
|
||||||
|
rand::rng().fill(&mut nonce_bytes[..]);
|
||||||
|
|
||||||
|
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||||
|
let cipher = Aes256Gcm::new(&key);
|
||||||
|
let nonce = Nonce::from(nonce_bytes);
|
||||||
|
|
||||||
|
let encrypted = cipher
|
||||||
|
.encrypt(&nonce, request.plaintext.as_ref())
|
||||||
|
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?;
|
||||||
|
|
||||||
|
// Ciphertext format: encrypted_data || nonce
|
||||||
|
let mut ciphertext = encrypted;
|
||||||
|
ciphertext.extend_from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
Ok(EncryptResponse {
|
||||||
|
ciphertext,
|
||||||
|
key_id: self.key_id.clone(),
|
||||||
|
key_version: 0,
|
||||||
|
algorithm: "AES-256-GCM".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||||
|
if request.ciphertext.len() < NONCE_SIZE + 1 {
|
||||||
|
return Err(KmsError::cryptographic_error("decrypt", "Ciphertext too short for static KMS format"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split ciphertext: encrypted_data || nonce(12)
|
||||||
|
let split_at = request.ciphertext.len() - NONCE_SIZE;
|
||||||
|
let encrypted = &request.ciphertext[..split_at];
|
||||||
|
let nonce_slice = &request.ciphertext[split_at..];
|
||||||
|
|
||||||
|
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||||
|
let cipher = Aes256Gcm::new(&key);
|
||||||
|
let nonce = Nonce::try_from(nonce_slice).map_err(|_| KmsError::cryptographic_error("nonce", "invalid nonce length"))?;
|
||||||
|
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(&nonce, encrypted)
|
||||||
|
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM decrypt", e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_key(&self, key_id: &str, _algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
|
if key_id == self.key_id {
|
||||||
|
return Err(KmsError::key_already_exists(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = self.key_metadata();
|
||||||
|
Ok(KeyInfo {
|
||||||
|
key_id: metadata.key_id,
|
||||||
|
description: metadata.description,
|
||||||
|
algorithm: "AES_256".to_string(),
|
||||||
|
usage: metadata.key_usage,
|
||||||
|
status: KeyStatus::Active,
|
||||||
|
version: 1,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
tags: HashMap::new(),
|
||||||
|
created_at: metadata.creation_date,
|
||||||
|
rotated_at: None,
|
||||||
|
created_by: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||||
|
let key_info = KeyInfo {
|
||||||
|
key_id: self.key_id.clone(),
|
||||||
|
description: Some("Static single-key KMS backend".to_string()),
|
||||||
|
algorithm: "AES_256".to_string(),
|
||||||
|
usage: KeyUsage::EncryptDecrypt,
|
||||||
|
status: KeyStatus::Active,
|
||||||
|
version: 1,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
tags: HashMap::new(),
|
||||||
|
created_at: Zoned::now(),
|
||||||
|
rotated_at: None,
|
||||||
|
created_by: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply prefix filter if provided
|
||||||
|
if let Some(ref marker) = request.marker
|
||||||
|
&& self.key_id <= *marker
|
||||||
|
{
|
||||||
|
return Ok(ListKeysResponse {
|
||||||
|
keys: vec![],
|
||||||
|
next_marker: None,
|
||||||
|
truncated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ListKeysResponse {
|
||||||
|
keys: vec![key_info],
|
||||||
|
next_marker: None,
|
||||||
|
truncated: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
// Static KMS key is always enabled
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot disable keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn schedule_key_deletion(
|
||||||
|
&self,
|
||||||
|
key_id: &str,
|
||||||
|
_pending_window_days: u32,
|
||||||
|
_context: Option<&OperationContext>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot schedule key deletion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||||
|
if key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot rotate keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check(&self) -> Result<()> {
|
||||||
|
// Static KMS is always healthy if it was successfully initialized
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backend_info(&self) -> BackendInfo {
|
||||||
|
BackendInfo::new("static".to_string(), env!("CARGO_PKG_VERSION").to_string(), "local".to_string(), true)
|
||||||
|
.with_metadata("key_id".to_string(), self.key_id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl KmsBackend for StaticKmsBackend {
|
||||||
|
async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
|
||||||
|
let key_name = request.key_name.as_deref().unwrap_or("");
|
||||||
|
if key_name == self.key_id {
|
||||||
|
return Err(KmsError::key_already_exists(&self.key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
|
||||||
|
<Self as KmsClient>::encrypt(self, &request, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
|
||||||
|
let key_id = self.key_id.clone();
|
||||||
|
let plaintext = <Self as KmsClient>::decrypt(self, &request, None).await?;
|
||||||
|
Ok(DecryptResponse {
|
||||||
|
plaintext,
|
||||||
|
key_id,
|
||||||
|
encryption_algorithm: Some("AES-256-GCM".to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||||
|
let gen_req = GenerateKeyRequest::new(request.key_id.clone(), request.key_spec.as_str().to_string());
|
||||||
|
let data_key = <Self as KmsClient>::generate_data_key(self, &gen_req, None).await?;
|
||||||
|
|
||||||
|
let plaintext_key = data_key
|
||||||
|
.plaintext
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| KmsError::internal_error("Generated data key is missing plaintext"))?;
|
||||||
|
|
||||||
|
Ok(GenerateDataKeyResponse {
|
||||||
|
key_id: data_key.key_id.clone(),
|
||||||
|
plaintext_key,
|
||||||
|
ciphertext_blob: data_key.ciphertext.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
||||||
|
let key_info = <Self as KmsClient>::describe_key(self, &request.key_id, None).await?;
|
||||||
|
let key_metadata = KeyMetadata {
|
||||||
|
key_id: key_info.key_id.clone(),
|
||||||
|
key_state: if key_info.status == KeyStatus::Active {
|
||||||
|
KeyState::Enabled
|
||||||
|
} else {
|
||||||
|
KeyState::Disabled
|
||||||
|
},
|
||||||
|
key_usage: key_info.usage,
|
||||||
|
description: key_info.description,
|
||||||
|
creation_date: key_info.created_at,
|
||||||
|
deletion_date: None,
|
||||||
|
origin: "EXTERNAL".to_string(),
|
||||||
|
key_manager: "STATIC".to_string(),
|
||||||
|
tags: key_info.tags,
|
||||||
|
};
|
||||||
|
Ok(DescribeKeyResponse { key_metadata })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
|
||||||
|
<Self as KmsClient>::list_keys(self, &request, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
|
||||||
|
if request.key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(&request.key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot delete keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result<CancelKeyDeletionResponse> {
|
||||||
|
if request.key_id != self.key_id {
|
||||||
|
return Err(KmsError::key_not_found(&request.key_id));
|
||||||
|
}
|
||||||
|
Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check(&self) -> Result<bool> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::backends::KmsClient;
|
||||||
|
use crate::config::{BackendConfig, KmsBackend, StaticConfig};
|
||||||
|
use base64::Engine as _;
|
||||||
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
|
|
||||||
|
/// Generate a random 32-byte key and return (key_id, raw_key).
|
||||||
|
fn random_static_key(key_id: &str) -> (String, [u8; 32]) {
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
rand::rng().fill(&mut key[..]);
|
||||||
|
(key_id.to_string(), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn static_config(key_id: &str, raw_key: &[u8; 32]) -> StaticConfig {
|
||||||
|
StaticConfig {
|
||||||
|
key_id: key_id.to_string(),
|
||||||
|
secret_key: BASE64.encode(raw_key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kms_config(config: StaticConfig) -> KmsConfig {
|
||||||
|
KmsConfig {
|
||||||
|
backend: KmsBackend::Static,
|
||||||
|
default_key_id: Some(config.key_id.clone()),
|
||||||
|
backend_config: BackendConfig::Static(config),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_test_backend() -> (StaticKmsBackend, String, [u8; 32]) {
|
||||||
|
let (key_id, key) = random_static_key("test-static-key");
|
||||||
|
let config = static_config(&key_id, &key);
|
||||||
|
let backend = StaticKmsBackend::new(kms_config(config))
|
||||||
|
.await
|
||||||
|
.expect("Failed to create backend");
|
||||||
|
(backend, key_id, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_generate_and_decrypt_data_key() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Generate data key
|
||||||
|
let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string())
|
||||||
|
.with_context("bucket".to_string(), "test-bucket".to_string());
|
||||||
|
let data_key = KmsClient::generate_data_key(&backend, &request, None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to generate data key");
|
||||||
|
|
||||||
|
assert_eq!(data_key.key_id, key_id);
|
||||||
|
assert_eq!(data_key.version, 0);
|
||||||
|
assert!(data_key.plaintext.is_some());
|
||||||
|
assert_eq!(data_key.plaintext.as_ref().expect("plaintext should be set").len(), 32);
|
||||||
|
// Ciphertext should be: encrypted(32) + tag(16) + nonce(12)
|
||||||
|
assert_eq!(data_key.ciphertext.len(), 32 + 16 + NONCE_SIZE);
|
||||||
|
|
||||||
|
// Decrypt the data key
|
||||||
|
let decrypt_request =
|
||||||
|
DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string());
|
||||||
|
let decrypted = KmsClient::decrypt(&backend, &decrypt_request, None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to decrypt");
|
||||||
|
|
||||||
|
assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_generate_data_key_wrong_key_id() {
|
||||||
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string());
|
||||||
|
let result = KmsClient::generate_data_key(&backend, &request, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_decrypt_invalid_ciphertext() {
|
||||||
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Ciphertext too short
|
||||||
|
let short = vec![0u8; 10];
|
||||||
|
let request = DecryptRequest::new(short);
|
||||||
|
let result = KmsClient::decrypt(&backend, &request, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_decrypt_tampered_ciphertext() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Generate a valid ciphertext first
|
||||||
|
let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string());
|
||||||
|
let data_key = KmsClient::generate_data_key(&backend, &gen_request, None)
|
||||||
|
.await
|
||||||
|
.expect("generate");
|
||||||
|
|
||||||
|
// Tamper with the ciphertext (flip a bit in the encrypted portion)
|
||||||
|
let mut tampered = data_key.ciphertext.clone();
|
||||||
|
if !tampered.is_empty() {
|
||||||
|
tampered[0] ^= 0x01;
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = DecryptRequest::new(tampered);
|
||||||
|
let result = KmsClient::decrypt(&backend, &request, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_key_returns_exists_for_configured_key() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Creating the pre-configured key should return KeyAlreadyExists
|
||||||
|
let result = KmsClient::create_key(&backend, &key_id, "AES_256", None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.expect_err("should be Err").to_string().contains("already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_key_returns_error_for_other_keys() {
|
||||||
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Creating any other key should return invalid operation (read-only)
|
||||||
|
let result = KmsClient::create_key(&backend, "other-key", "AES_256", None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.expect_err("should be Err").to_string();
|
||||||
|
assert!(err_msg.contains("read-only") || err_msg.contains("cannot create"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_describe_key() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let key_info = KmsClient::describe_key(&backend, &key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("describe_key should succeed");
|
||||||
|
assert_eq!(key_info.key_id, key_id);
|
||||||
|
assert_eq!(key_info.status, KeyStatus::Active);
|
||||||
|
assert_eq!(key_info.algorithm, "AES_256");
|
||||||
|
|
||||||
|
// Wrong key ID
|
||||||
|
let result = KmsClient::describe_key(&backend, "nonexistent", None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_keys() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let response = KmsClient::list_keys(&backend, &ListKeysRequest::default(), None)
|
||||||
|
.await
|
||||||
|
.expect("list_keys should succeed");
|
||||||
|
assert_eq!(response.keys.len(), 1);
|
||||||
|
assert_eq!(response.keys[0].key_id, key_id);
|
||||||
|
assert!(!response.truncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_disable_key_returns_error() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let result = KmsClient::disable_key(&backend, &key_id, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.expect_err("should be Err").to_string().contains("read-only"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_enable_key_is_noop() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Enable should succeed (no-op for static KMS)
|
||||||
|
KmsClient::enable_key(&backend, &key_id, None)
|
||||||
|
.await
|
||||||
|
.expect("enable_key should be no-op");
|
||||||
|
|
||||||
|
// Wrong key should still fail
|
||||||
|
let result = KmsClient::enable_key(&backend, "wrong", None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_key_returns_error() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let result = KmsClient::schedule_key_deletion(&backend, &key_id, 7, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.expect_err("should be Err").to_string().contains("read-only"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rotate_key_returns_error() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let result = KmsClient::rotate_key(&backend, &key_id, None).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check() {
|
||||||
|
let (backend, _key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
KmsClient::health_check(&backend).await.expect("health_check should succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_backend_info() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let info = KmsClient::backend_info(&backend);
|
||||||
|
assert_eq!(info.backend_type, "static");
|
||||||
|
assert_eq!(info.endpoint, "local");
|
||||||
|
assert!(info.healthy);
|
||||||
|
assert_eq!(info.metadata.get("key_id"), Some(&key_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_encrypt_decrypt_direct() {
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
let plaintext = b"Hello, static KMS world!";
|
||||||
|
let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec());
|
||||||
|
let enc_response = KmsClient::encrypt(&backend, &enc_request, None)
|
||||||
|
.await
|
||||||
|
.expect("encrypt should succeed");
|
||||||
|
|
||||||
|
assert_eq!(enc_response.key_id, key_id);
|
||||||
|
assert!(!enc_response.ciphertext.is_empty());
|
||||||
|
|
||||||
|
let dec_request = DecryptRequest::new(enc_response.ciphertext);
|
||||||
|
let decrypted = KmsClient::decrypt(&backend, &dec_request, None)
|
||||||
|
.await
|
||||||
|
.expect("decrypt should succeed");
|
||||||
|
|
||||||
|
assert_eq!(decrypted, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_kms_backend_trait_methods() {
|
||||||
|
use crate::backends::KmsBackend;
|
||||||
|
|
||||||
|
let (backend, key_id, _key) = create_test_backend().await;
|
||||||
|
|
||||||
|
// Test KmsBackend::generate_data_key
|
||||||
|
let gen_req = GenerateDataKeyRequest::new(key_id.clone(), KeySpec::Aes256);
|
||||||
|
let gen_resp = KmsBackend::generate_data_key(&backend, gen_req)
|
||||||
|
.await
|
||||||
|
.expect("KmsBackend::generate_data_key should succeed");
|
||||||
|
assert_eq!(gen_resp.key_id, key_id);
|
||||||
|
assert_eq!(gen_resp.plaintext_key.len(), 32);
|
||||||
|
assert!(!gen_resp.ciphertext_blob.is_empty());
|
||||||
|
|
||||||
|
// Test KmsBackend::decrypt via the generated ciphertext
|
||||||
|
let dec_req = DecryptRequest::new(gen_resp.ciphertext_blob);
|
||||||
|
let dec_resp = KmsBackend::decrypt(&backend, dec_req)
|
||||||
|
.await
|
||||||
|
.expect("KmsBackend::decrypt should succeed");
|
||||||
|
assert_eq!(dec_resp.plaintext, gen_resp.plaintext_key);
|
||||||
|
|
||||||
|
// Test KmsBackend::describe_key
|
||||||
|
let desc_req = DescribeKeyRequest { key_id: key_id.clone() };
|
||||||
|
let desc_resp = KmsBackend::describe_key(&backend, desc_req)
|
||||||
|
.await
|
||||||
|
.expect("describe_key should succeed");
|
||||||
|
assert_eq!(desc_resp.key_metadata.key_id, key_id);
|
||||||
|
|
||||||
|
// Test KmsBackend::create_key for the configured key (should return KeyAlreadyExists, same as KmsClient)
|
||||||
|
let create_req = CreateKeyRequest {
|
||||||
|
key_name: Some(key_id.clone()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let create_err = KmsBackend::create_key(&backend, create_req)
|
||||||
|
.await
|
||||||
|
.expect_err("create_key for configured key should return KeyAlreadyExists");
|
||||||
|
assert!(create_err.to_string().contains("already exists"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -600,7 +600,9 @@ impl VaultKmsBackend {
|
|||||||
|
|
||||||
let vault_config = match &config.backend_config {
|
let vault_config = match &config.backend_config {
|
||||||
crate::config::BackendConfig::VaultKv2(vault_config) => (**vault_config).clone(),
|
crate::config::BackendConfig::VaultKv2(vault_config) => (**vault_config).clone(),
|
||||||
crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::VaultTransit(_) => {
|
crate::config::BackendConfig::Local(_)
|
||||||
|
| crate::config::BackendConfig::VaultTransit(_)
|
||||||
|
| crate::config::BackendConfig::Static(_) => {
|
||||||
return Err(KmsError::configuration_error("Expected Vault KV2 backend configuration"));
|
return Err(KmsError::configuration_error("Expected Vault KV2 backend configuration"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ impl VaultTransitKmsBackend {
|
|||||||
metadata_key_prefix: vault_config.key_path_prefix.clone(),
|
metadata_key_prefix: vault_config.key_path_prefix.clone(),
|
||||||
tls: vault_config.tls.clone(),
|
tls: vault_config.tls.clone(),
|
||||||
},
|
},
|
||||||
crate::config::BackendConfig::Local(_) => {
|
crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::Static(_) => {
|
||||||
return Err(KmsError::configuration_error("Expected Vault Transit backend configuration"));
|
return Err(KmsError::configuration_error("Expected Vault Transit backend configuration"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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_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_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_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_KV_MOUNT: &str = "secret";
|
||||||
pub const DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX: &str = "rustfs/kms/transit-metadata";
|
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,
|
RedactionLevel::Secret,
|
||||||
"admin configure request vault transit approle 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) const REDACTED_SECRET: &str = "***redacted***";
|
||||||
@@ -97,6 +100,9 @@ pub enum KmsBackend {
|
|||||||
/// Local file-based backend for development and testing only
|
/// Local file-based backend for development and testing only
|
||||||
#[default]
|
#[default]
|
||||||
Local,
|
Local,
|
||||||
|
/// Static single-key backend that derives DEKs from a pre-configured key
|
||||||
|
#[serde(rename = "Static")]
|
||||||
|
Static,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main KMS configuration
|
/// Main KMS configuration
|
||||||
@@ -146,6 +152,8 @@ pub enum BackendConfig {
|
|||||||
VaultKv2(Box<VaultConfig>),
|
VaultKv2(Box<VaultConfig>),
|
||||||
/// Vault Transit backend configuration
|
/// Vault Transit backend configuration
|
||||||
VaultTransit(Box<VaultTransitConfig>),
|
VaultTransit(Box<VaultTransitConfig>),
|
||||||
|
/// Static single-key backend configuration
|
||||||
|
Static(StaticConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BackendConfig {
|
impl Default for BackendConfig {
|
||||||
@@ -160,6 +168,7 @@ impl fmt::Debug for BackendConfig {
|
|||||||
Self::Local(config) => f.debug_tuple("Local").field(config).finish(),
|
Self::Local(config) => f.debug_tuple("Local").field(config).finish(),
|
||||||
Self::VaultKv2(config) => f.debug_tuple("VaultKv2").field(config).finish(),
|
Self::VaultKv2(config) => f.debug_tuple("VaultKv2").field(config).finish(),
|
||||||
Self::VaultTransit(config) => f.debug_tuple("VaultTransit").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)
|
/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit)
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct VaultConfig {
|
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
|
/// Get the local configuration if backend is Local
|
||||||
pub fn local_config(&self) -> Option<&LocalConfig> {
|
pub fn local_config(&self) -> Option<&LocalConfig> {
|
||||||
match &self.backend_config {
|
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
|
/// Set default key ID
|
||||||
pub fn with_default_key(mut self, key_id: String) -> Self {
|
pub fn with_default_key(mut self, key_id: String) -> Self {
|
||||||
self.default_key_id = Some(key_id);
|
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");
|
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
|
// Validate cache configuration
|
||||||
@@ -564,6 +649,7 @@ impl KmsConfig {
|
|||||||
"local" => KmsBackend::Local,
|
"local" => KmsBackend::Local,
|
||||||
"vault" | "vault-kv2" | "vault_kv2" => KmsBackend::VaultKv2,
|
"vault" | "vault-kv2" | "vault_kv2" => KmsBackend::VaultKv2,
|
||||||
"vault-transit" | "vault_transit" => KmsBackend::VaultTransit,
|
"vault-transit" | "vault_transit" => KmsBackend::VaultTransit,
|
||||||
|
"static" => KmsBackend::Static,
|
||||||
_ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {backend_type}"))),
|
_ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {backend_type}"))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -641,6 +727,47 @@ impl KmsConfig {
|
|||||||
tls: vault_tls_config(skip_tls_verify),
|
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()?;
|
config.validate()?;
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ pub mod types;
|
|||||||
|
|
||||||
// Re-export public API
|
// Re-export public API
|
||||||
pub use api_types::{
|
pub use api_types::{
|
||||||
CacheSummary, ConfigureKmsRequest, ConfigureKmsResponse, ConfigureLocalKmsRequest, ConfigureVaultKmsRequest,
|
CacheSummary, ConfigureKmsRequest, ConfigureKmsResponse, ConfigureLocalKmsRequest, ConfigureStaticKmsRequest,
|
||||||
ConfigureVaultTransitKmsRequest, KmsConfigSummary, KmsStatusResponse, StartKmsRequest, StartKmsResponse, StopKmsResponse,
|
ConfigureVaultKmsRequest, ConfigureVaultTransitKmsRequest, KmsConfigSummary, KmsStatusResponse, StartKmsRequest,
|
||||||
TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse,
|
StartKmsResponse, StopKmsResponse, TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse,
|
||||||
|
UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse,
|
||||||
};
|
};
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use encryption::is_data_key_envelope;
|
pub use encryption::is_data_key_envelope;
|
||||||
|
|||||||
@@ -384,6 +384,11 @@ impl KmsServiceManager {
|
|||||||
let backend = crate::backends::vault_transit::VaultTransitKmsBackend::new(config.clone()).await?;
|
let backend = crate::backends::vault_transit::VaultTransitKmsBackend::new(config.clone()).await?;
|
||||||
Arc::new(backend) as Arc<dyn KmsBackend>
|
Arc::new(backend) as Arc<dyn KmsBackend>
|
||||||
}
|
}
|
||||||
|
BackendConfig::Static(_) => {
|
||||||
|
info!("Creating Static KMS backend for version {}", version);
|
||||||
|
let backend = crate::backends::static_kms::StaticKmsBackend::new(config.clone()).await?;
|
||||||
|
Arc::new(backend) as Arc<dyn KmsBackend>
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create KMS manager
|
// Create KMS manager
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ fn existing_vault_auth(config: &KmsConfig) -> Option<rustfs_kms::config::VaultAu
|
|||||||
rustfs_kms::config::BackendConfig::VaultKv2(vault) => Some(vault.auth_method.clone()),
|
rustfs_kms::config::BackendConfig::VaultKv2(vault) => Some(vault.auth_method.clone()),
|
||||||
rustfs_kms::config::BackendConfig::VaultTransit(vault) => Some(vault.auth_method.clone()),
|
rustfs_kms::config::BackendConfig::VaultTransit(vault) => Some(vault.auth_method.clone()),
|
||||||
rustfs_kms::config::BackendConfig::Local(_) => None,
|
rustfs_kms::config::BackendConfig::Local(_) => None,
|
||||||
|
rustfs_kms::config::BackendConfig::Static(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ fn normalize_configure_request_auth(
|
|||||||
ConfigureKmsRequest::VaultKv2(req) => token_is_blank(&req.auth_method),
|
ConfigureKmsRequest::VaultKv2(req) => token_is_blank(&req.auth_method),
|
||||||
ConfigureKmsRequest::VaultTransit(req) => token_is_blank(&req.auth_method),
|
ConfigureKmsRequest::VaultTransit(req) => token_is_blank(&req.auth_method),
|
||||||
ConfigureKmsRequest::Local(_) => false,
|
ConfigureKmsRequest::Local(_) => false,
|
||||||
|
ConfigureKmsRequest::Static(_) => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !needs_existing_auth {
|
if !needs_existing_auth {
|
||||||
@@ -98,6 +100,7 @@ fn normalize_configure_request_auth(
|
|||||||
ConfigureKmsRequest::VaultKv2(req) => req.auth_method = existing_auth,
|
ConfigureKmsRequest::VaultKv2(req) => req.auth_method = existing_auth,
|
||||||
ConfigureKmsRequest::VaultTransit(req) => req.auth_method = existing_auth,
|
ConfigureKmsRequest::VaultTransit(req) => req.auth_method = existing_auth,
|
||||||
ConfigureKmsRequest::Local(_) => {}
|
ConfigureKmsRequest::Local(_) => {}
|
||||||
|
ConfigureKmsRequest::Static(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ fn backend_name(backend: &KmsBackend) -> &'static str {
|
|||||||
KmsBackend::Local => "local",
|
KmsBackend::Local => "local",
|
||||||
KmsBackend::VaultKv2 => "vault-kv2",
|
KmsBackend::VaultKv2 => "vault-kv2",
|
||||||
KmsBackend::VaultTransit => "vault-transit",
|
KmsBackend::VaultTransit => "vault-transit",
|
||||||
|
KmsBackend::Static => "static",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+70
-6
@@ -283,7 +283,7 @@ fn build_local_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| Error::other("KMS key directory is required for local backend"))?;
|
.ok_or_else(|| Error::other("KMS key directory is required for local backend"))?;
|
||||||
|
|
||||||
Ok(rustfs_kms::config::KmsConfig {
|
let kms_config = rustfs_kms::config::KmsConfig {
|
||||||
backend: rustfs_kms::config::KmsBackend::Local,
|
backend: rustfs_kms::config::KmsBackend::Local,
|
||||||
backend_config: rustfs_kms::config::BackendConfig::Local(rustfs_kms::config::LocalConfig {
|
backend_config: rustfs_kms::config::BackendConfig::Local(rustfs_kms::config::LocalConfig {
|
||||||
key_dir: std::path::PathBuf::from(key_dir),
|
key_dir: std::path::PathBuf::from(key_dir),
|
||||||
@@ -296,7 +296,11 @@ fn build_local_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
|||||||
retry_attempts: 3,
|
retry_attempts: 3,
|
||||||
enable_cache: true,
|
enable_cache: true,
|
||||||
cache_config: rustfs_kms::config::CacheConfig::default(),
|
cache_config: rustfs_kms::config::CacheConfig::default(),
|
||||||
})
|
};
|
||||||
|
kms_config
|
||||||
|
.validate()
|
||||||
|
.map_err(|e| Error::other(format!("Local KMS configuration validation failed: {e}")))?;
|
||||||
|
Ok(kms_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build KMS configuration for Vault backend
|
/// Build KMS configuration for Vault backend
|
||||||
@@ -310,7 +314,7 @@ fn build_vault_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| Error::other("Vault token is required for vault backend"))?;
|
.ok_or_else(|| Error::other("Vault token is required for vault backend"))?;
|
||||||
|
|
||||||
Ok(rustfs_kms::config::KmsConfig {
|
let kms_config = rustfs_kms::config::KmsConfig {
|
||||||
backend: rustfs_kms::config::KmsBackend::VaultKv2,
|
backend: rustfs_kms::config::KmsBackend::VaultKv2,
|
||||||
backend_config: rustfs_kms::config::BackendConfig::VaultKv2(Box::new(rustfs_kms::config::VaultConfig {
|
backend_config: rustfs_kms::config::BackendConfig::VaultKv2(Box::new(rustfs_kms::config::VaultConfig {
|
||||||
address: vault_address.clone(),
|
address: vault_address.clone(),
|
||||||
@@ -329,7 +333,11 @@ fn build_vault_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::c
|
|||||||
retry_attempts: 3,
|
retry_attempts: 3,
|
||||||
enable_cache: true,
|
enable_cache: true,
|
||||||
cache_config: rustfs_kms::config::CacheConfig::default(),
|
cache_config: rustfs_kms::config::CacheConfig::default(),
|
||||||
})
|
};
|
||||||
|
kms_config
|
||||||
|
.validate()
|
||||||
|
.map_err(|e| Error::other(format!("Vault KMS configuration validation failed: {e}")))?;
|
||||||
|
Ok(kms_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build KMS configuration for Vault Transit backend
|
/// Build KMS configuration for Vault Transit backend
|
||||||
@@ -343,7 +351,7 @@ fn build_vault_transit_kms_config(cfg: &config::Config) -> std::io::Result<rustf
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| Error::other("Vault token is required for vault-transit backend"))?;
|
.ok_or_else(|| Error::other("Vault token is required for vault-transit backend"))?;
|
||||||
|
|
||||||
Ok(rustfs_kms::config::KmsConfig {
|
let kms_config = rustfs_kms::config::KmsConfig {
|
||||||
backend: rustfs_kms::config::KmsBackend::VaultTransit,
|
backend: rustfs_kms::config::KmsBackend::VaultTransit,
|
||||||
backend_config: rustfs_kms::config::BackendConfig::VaultTransit(Box::new(rustfs_kms::config::VaultTransitConfig {
|
backend_config: rustfs_kms::config::BackendConfig::VaultTransit(Box::new(rustfs_kms::config::VaultTransitConfig {
|
||||||
address: vault_address.clone(),
|
address: vault_address.clone(),
|
||||||
@@ -360,7 +368,62 @@ fn build_vault_transit_kms_config(cfg: &config::Config) -> std::io::Result<rustf
|
|||||||
retry_attempts: 3,
|
retry_attempts: 3,
|
||||||
enable_cache: true,
|
enable_cache: true,
|
||||||
cache_config: rustfs_kms::config::CacheConfig::default(),
|
cache_config: rustfs_kms::config::CacheConfig::default(),
|
||||||
})
|
};
|
||||||
|
kms_config
|
||||||
|
.validate()
|
||||||
|
.map_err(|e| Error::other(format!("Vault Transit KMS configuration validation failed: {e}")))?;
|
||||||
|
Ok(kms_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build KMS configuration for static single-key backend
|
||||||
|
fn build_static_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::config::KmsConfig> {
|
||||||
|
use rustfs_kms::config::{ENV_KMS_STATIC_SECRET_KEY, ENV_KMS_STATIC_SECRET_KEY_FILE, StaticConfig};
|
||||||
|
|
||||||
|
// Read secret from file first, then fall back to env var
|
||||||
|
let secret_str = if let Some(file_path) = rustfs_utils::get_env_opt_str(ENV_KMS_STATIC_SECRET_KEY_FILE) {
|
||||||
|
std::fs::read_to_string(&file_path)
|
||||||
|
.map_err(|e| Error::other(format!("Failed to read static KMS secret key file {file_path}: {e}")))?
|
||||||
|
} else {
|
||||||
|
rustfs_utils::get_env_str(ENV_KMS_STATIC_SECRET_KEY, "")
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_str = secret_str.trim();
|
||||||
|
if secret_str.is_empty() {
|
||||||
|
return Err(Error::other(format!(
|
||||||
|
"Static KMS requires {ENV_KMS_STATIC_SECRET_KEY} or {ENV_KMS_STATIC_SECRET_KEY_FILE} to be set"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let colon_pos = secret_str.find(':').ok_or_else(|| {
|
||||||
|
Error::other(format!(
|
||||||
|
"Static KMS secret key must be in format <key-name>:<base64-key>, got: {secret_str}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let key_id = secret_str[..colon_pos].to_string();
|
||||||
|
let secret_key = secret_str[colon_pos + 1..].to_string();
|
||||||
|
|
||||||
|
if key_id.is_empty() || secret_key.is_empty() {
|
||||||
|
return Err(Error::other("Static KMS secret key must be in format <key-name>:<base64-key>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 decoding and 32-byte key length are validated by KmsConfig::validate() below
|
||||||
|
let static_config = StaticConfig {
|
||||||
|
key_id: key_id.clone(),
|
||||||
|
secret_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
let kms_config = rustfs_kms::config::KmsConfig {
|
||||||
|
backend: rustfs_kms::config::KmsBackend::Static,
|
||||||
|
default_key_id: cfg.kms_default_key_id.clone().or(Some(key_id)),
|
||||||
|
backend_config: rustfs_kms::config::BackendConfig::Static(static_config),
|
||||||
|
allow_insecure_dev_defaults: cfg.kms_allow_insecure_dev_defaults,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
kms_config
|
||||||
|
.validate()
|
||||||
|
.map_err(|e| Error::other(format!("Static KMS configuration validation failed: {e}")))?;
|
||||||
|
Ok(kms_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure and start KMS service
|
/// Configure and start KMS service
|
||||||
@@ -423,6 +486,7 @@ pub async fn init_kms_system(config: &config::Config) -> std::io::Result<()> {
|
|||||||
"local" => build_local_kms_config(config)?,
|
"local" => build_local_kms_config(config)?,
|
||||||
"vault" | "vault-kv2" | "vault_kv2" => build_vault_kms_config(config)?,
|
"vault" | "vault-kv2" | "vault_kv2" => build_vault_kms_config(config)?,
|
||||||
"vault-transit" | "vault_transit" => build_vault_transit_kms_config(config)?,
|
"vault-transit" | "vault_transit" => build_vault_transit_kms_config(config)?,
|
||||||
|
"static" => build_static_kms_config(config)?,
|
||||||
_ => return Err(Error::other(format!("Unsupported KMS backend: {}", config.kms_backend))),
|
_ => return Err(Error::other(format!("Unsupported KMS backend: {}", config.kms_backend))),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1601,12 +1601,21 @@ async fn apply_managed_encryption_material(
|
|||||||
// Try to get default key from KMS service (if available)
|
// Try to get default key from KMS service (if available)
|
||||||
if let Some(service) = runtime_sources::current_encryption_service().await {
|
if let Some(service) = runtime_sources::current_encryption_service().await {
|
||||||
kms_key_candidate = service.get_default_key_id().cloned();
|
kms_key_candidate = service.get_default_key_id().cloned();
|
||||||
|
tracing::debug!(
|
||||||
|
default_key_id = ?kms_key_candidate,
|
||||||
|
"SSE-S3: KMS service available, default_key_id from config"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::debug!("SSE-S3: KMS encryption service not available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kms_key_to_use = match (encryption_type, kms_key_candidate.clone()) {
|
let kms_key_to_use = match (encryption_type, kms_key_candidate.clone()) {
|
||||||
(SSEType::SseS3, Some(kms_key_id)) => kms_key_id,
|
(SSEType::SseS3, Some(kms_key_id)) => kms_key_id,
|
||||||
(SSEType::SseS3, None) => "default".to_string(),
|
(SSEType::SseS3, None) => {
|
||||||
|
tracing::debug!("SSE-S3: no KMS key configured, falling back to \"default\" key ID");
|
||||||
|
"default".to_string()
|
||||||
|
}
|
||||||
(SSEType::SseKms, Some(kms_key_id)) => kms_key_id,
|
(SSEType::SseKms, Some(kms_key_id)) => kms_key_id,
|
||||||
(SSEType::SseKms, None) => {
|
(SSEType::SseKms, None) => {
|
||||||
return Err(ApiError::from(StorageError::other(
|
return Err(ApiError::from(StorageError::other(
|
||||||
@@ -1635,7 +1644,7 @@ async fn apply_managed_encryption_material(
|
|||||||
Ok(EncryptionMaterial {
|
Ok(EncryptionMaterial {
|
||||||
sse_type: encryption_type,
|
sse_type: encryption_type,
|
||||||
server_side_encryption,
|
server_side_encryption,
|
||||||
kms_key_id: matches!(encryption_type, SSEType::SseKms).then_some(kms_key_to_use),
|
kms_key_id: Some(kms_key_to_use),
|
||||||
algorithm,
|
algorithm,
|
||||||
key_bytes,
|
key_bytes,
|
||||||
base_nonce,
|
base_nonce,
|
||||||
@@ -3523,7 +3532,7 @@ mod tests {
|
|||||||
let material = material.expect("managed sse-s3 encryption should return material");
|
let material = material.expect("managed sse-s3 encryption should return material");
|
||||||
let metadata = encryption_material_to_metadata(&material).expect("managed SSE-S3 metadata should serialize");
|
let metadata = encryption_material_to_metadata(&material).expect("managed SSE-S3 metadata should serialize");
|
||||||
|
|
||||||
assert_eq!(material.kms_key_id, None);
|
assert_eq!(material.kms_key_id.as_deref(), Some("default"));
|
||||||
assert_eq!(metadata.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
|
assert_eq!(metadata.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
|
||||||
assert!(!metadata.contains_key("x-amz-server-side-encryption-aws-kms-key-id"));
|
assert!(!metadata.contains_key("x-amz-server-side-encryption-aws-kms-key-id"));
|
||||||
assert_eq!(metadata.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER).map(String::as_str), Some("default"));
|
assert_eq!(metadata.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER).map(String::as_str), Some("default"));
|
||||||
|
|||||||
Reference in New Issue
Block a user