mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 14:49:25 +00:00
8387528c9b
* feat(kms): record real cache hit, miss and eviction metrics The metadata cache reported (entry_count, 0) because moka exposes no hit or miss counts, so the miss half of every cache report was a constant. Track lookups and removals in the cache itself: hit/miss counters on the lookup path, a moka eviction listener classifying removals by cause, and an entry gauge refreshed whenever the entry set changes. The counters are exported through the metrics facade under the rustfs_kms_ prefix with static label values only, matching the operation-policy metrics, and are also returned as a KmsCacheStats snapshot in place of the old tuple. Cache semantics are unchanged: capacity, TTL and invalidation points are the same, and remove now flushes pending maintenance so the gauge and the removal notification describe the cache the caller sees. Refs rustfs/backlog#1584 * fix(kms): report real cache counters through the admin status API KmsStatusResponse.cache_stats mapped the old (entry_count, 0) tuple onto hit_count and miss_count, so operators polling KMS status read the entry count as a hit count and a miss count that was always zero. Map the fields to the counters they claim to be, and add entry_count and eviction_count as additive, defaulted fields so the entry number that hit_count used to carry is still available. Refs rustfs/backlog#1584 * fix(kms): refresh the cache entry gauge on lookup misses The entry gauge was published only from the write paths, so an entry dropped by TTL expiry left `rustfs_kms_metadata_cache_entries` reporting a population that no longer existed until the next put, remove or clear. A cache that goes quiet — entries ageing out with no further writes — kept over-reporting indefinitely. Republish the gauge from the lookup path when the lookup misses. A miss is where expiry surfaces, and moka reaps expired entries in the maintenance it runs during that same lookup, so the count read afterwards reflects the reaping. Hits stay free of the extra work. * docs(kms): correct the entry gauge convergence claim on the miss path The comment on the miss-path gauge refresh said moka reaps expired entries in the maintenance it runs on that same lookup. It does not: `should_apply_reads` is gated on a full read log or an elapsed housekeeping interval, so the removal that decrements `entry_count` and reaches the eviction listener may land on a later lookup. The behaviour and the test are unchanged — the gauge still converges, and the test drives `run_pending_tasks` explicitly rather than riding on that interval. Only the stated guarantee was wrong, so say interval instead of same-lookup and record why forcing maintenance on the read path was not the trade taken.
251 lines
9.7 KiB
Rust
251 lines
9.7 KiB
Rust
#![deny(clippy::unwrap_used)]
|
|
// 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.
|
|
|
|
//! # RustFS Key Management Service (KMS)
|
|
//!
|
|
//! This crate provides a comprehensive Key Management Service (KMS) for RustFS,
|
|
//! supporting secure key generation, storage, and object encryption capabilities.
|
|
//!
|
|
//! ## Features
|
|
//!
|
|
//! - **Multiple Backends**: Local file storage, Vault KV2 (plain KV storage), and Vault Transit (optional)
|
|
//! - **Object Encryption**: Transparent S3-compatible object encryption
|
|
//! - **Streaming Encryption**: Memory-efficient encryption for large files
|
|
//! - **Key Management**: Full lifecycle management of encryption keys
|
|
//! - **S3 Compatibility**: SSE-S3, SSE-KMS, and SSE-C encryption modes
|
|
//!
|
|
//! ## Architecture
|
|
//!
|
|
//! The KMS follows a three-layer key hierarchy:
|
|
//! - **Master Keys**: Managed by KMS backends (Local / Vault KV2 / Vault Transit)
|
|
//! - **Data Encryption Keys (DEK)**: Generated per object, encrypted by master keys
|
|
//! - **Object Data**: Encrypted using DEKs with AES-256-GCM or ChaCha20-Poly1305
|
|
//!
|
|
//! ## Caching Discipline
|
|
//!
|
|
//! KMS may cache stable master-key metadata, but it must not cache or reuse generated
|
|
//! data encryption keys by master key id alone. A generated DEK and its encrypted
|
|
//! ciphertext can be bound to the object encryption context, such as the bucket and
|
|
//! object path. Reusing it for another object can break context validation and would
|
|
//! also violate the expected per-object DEK model for SSE-S3 and SSE-KMS.
|
|
//!
|
|
//! ## Example
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use rustfs_kms::{KmsConfig, init_global_kms_service_manager};
|
|
//! use std::path::PathBuf;
|
|
//!
|
|
//! #[tokio::main]
|
|
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! // Initialize global KMS service manager
|
|
//! let service_manager = init_global_kms_service_manager();
|
|
//!
|
|
//! // Configure with local backend
|
|
//! let config = KmsConfig::local(PathBuf::from("./kms_keys")).with_insecure_development_defaults();
|
|
//! service_manager.configure(config).await?;
|
|
//!
|
|
//! // Start the KMS service
|
|
//! service_manager.start().await?;
|
|
//!
|
|
//! Ok(())
|
|
//! }
|
|
//! ```
|
|
|
|
// Core modules
|
|
pub mod api_types;
|
|
pub mod audit;
|
|
pub mod backends;
|
|
pub mod backup;
|
|
mod cache;
|
|
pub mod config;
|
|
pub mod deletion_worker;
|
|
mod encryption;
|
|
mod error;
|
|
pub mod manager;
|
|
mod policy;
|
|
pub mod probe;
|
|
pub mod service;
|
|
pub mod service_manager;
|
|
mod time_serde;
|
|
pub mod types;
|
|
|
|
// Re-export public API
|
|
pub use api_types::{
|
|
CacheSummary, ConfigureKmsRequest, ConfigureKmsResponse, ConfigureLocalKmsRequest, ConfigureStaticKmsRequest,
|
|
ConfigureVaultKmsRequest, ConfigureVaultTransitKmsRequest, KmsConfigSummary, KmsStatusResponse, StartKmsRequest,
|
|
StartKmsResponse, StopKmsResponse, TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse,
|
|
UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse,
|
|
};
|
|
pub use audit::{KmsAuditOperation, KmsAuditOutcome, KmsAuditRecord, KmsAuditSink, redact_encryption_context};
|
|
pub use cache::KmsCacheStats;
|
|
pub use config::*;
|
|
pub use deletion_worker::DeletionReferenceChecker;
|
|
pub use encryption::is_data_key_envelope;
|
|
pub use error::{KmsError, KmsUnavailableError, Result};
|
|
pub use manager::KmsManager;
|
|
pub use probe::{ProbeFailureKind, ProbeResult, ProbeStatus};
|
|
pub use service::{DataKey, ObjectEncryptionService};
|
|
pub use service_manager::{
|
|
KmsServiceManager, KmsServiceStatus, KmsStartOutcome, get_global_encryption_service, get_global_kms_service_manager,
|
|
init_global_kms_service_manager,
|
|
};
|
|
pub use types::*;
|
|
|
|
// For backward compatibility - these functions now delegate to the service manager
|
|
|
|
/// Initialize global encryption service (backward compatibility)
|
|
///
|
|
/// This function is now deprecated. Use `init_global_kms_service_manager` and configure via API instead.
|
|
#[deprecated(note = "Use dynamic KMS configuration via service manager instead")]
|
|
pub async fn init_global_services(_service: ObjectEncryptionService) -> Result<()> {
|
|
// For backward compatibility only - not recommended for new code
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if the global encryption service is initialized and healthy
|
|
pub async fn is_encryption_service_healthy() -> bool {
|
|
match get_global_encryption_service().await {
|
|
Some(service) => service.health_check().await.is_ok(),
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Shutdown the global encryption service (backward compatibility)
|
|
#[deprecated(note = "Use service manager shutdown instead")]
|
|
pub fn shutdown_global_services() {
|
|
// For backward compatibility only - service manager handles shutdown now
|
|
tracing::info!("KMS global services shutdown requested (deprecated)");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Arc;
|
|
use tempfile::TempDir;
|
|
|
|
#[tokio::test]
|
|
async fn test_global_service_lifecycle() {
|
|
// Test service manager initialization
|
|
let manager = init_global_kms_service_manager();
|
|
|
|
// Test initial status
|
|
let status = manager.get_status().await;
|
|
assert_eq!(status, KmsServiceStatus::NotConfigured);
|
|
|
|
// Test configuration and start
|
|
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();
|
|
|
|
manager.configure(config).await.expect("Configuration should succeed");
|
|
manager.start().await.expect("Start should succeed");
|
|
|
|
// Test that encryption service is now available
|
|
assert!(get_global_encryption_service().await.is_some());
|
|
|
|
// Test health check
|
|
assert!(is_encryption_service_healthy().await);
|
|
|
|
// Test stop
|
|
manager.stop().await.expect("Stop should succeed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_versioned_service_reconfiguration() {
|
|
// Test versioned service reconfiguration for zero-downtime
|
|
let manager = KmsServiceManager::new();
|
|
|
|
// Initial state: no version
|
|
assert!(manager.get_service_version().await.is_none());
|
|
|
|
// Start first service
|
|
let temp_dir1 = TempDir::new().expect("Failed to create temp dir");
|
|
let config1 = KmsConfig::local(temp_dir1.path().to_path_buf()).with_insecure_development_defaults();
|
|
manager
|
|
.configure(config1.clone())
|
|
.await
|
|
.expect("Configuration should succeed");
|
|
manager.start().await.expect("Start should succeed");
|
|
|
|
// Verify version 1
|
|
let version1 = manager.get_service_version().await.expect("Service should have version");
|
|
assert_eq!(version1, 1);
|
|
|
|
// Get service reference (simulating ongoing operation)
|
|
let service1 = manager.get_encryption_service().await.expect("Service should be available");
|
|
|
|
// Reconfigure to new service (zero-downtime)
|
|
let mut config2 = config1;
|
|
config2.timeout = std::time::Duration::from_secs(45);
|
|
manager.reconfigure(config2).await.expect("Reconfiguration should succeed");
|
|
|
|
// Verify version 2
|
|
let version2 = manager.get_service_version().await.expect("Service should have version");
|
|
assert_eq!(version2, 2);
|
|
|
|
// Old service reference should still be valid (Arc keeps it alive)
|
|
// New requests should get version 2
|
|
let service2 = manager.get_encryption_service().await.expect("Service should be available");
|
|
|
|
// Verify they are different instances
|
|
assert!(!Arc::ptr_eq(&service1, &service2));
|
|
|
|
// Old service should still work (simulating long-running operation)
|
|
// This demonstrates zero-downtime: old operations continue, new operations use new service
|
|
assert!(service1.health_check().await.is_ok());
|
|
assert!(service2.health_check().await.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_reconfiguration() {
|
|
// Test that concurrent reconfiguration requests are serialized
|
|
let manager = Arc::new(KmsServiceManager::new());
|
|
|
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let base_path = temp_dir.path().to_path_buf();
|
|
|
|
// Initial configuration
|
|
let config1 = KmsConfig::local(base_path.clone()).with_insecure_development_defaults();
|
|
manager.configure(config1).await.expect("Configuration should succeed");
|
|
manager.start().await.expect("Start should succeed");
|
|
|
|
// Spawn multiple concurrent reconfiguration requests
|
|
let mut handles = Vec::new();
|
|
for _i in 0..5 {
|
|
let manager_clone = manager.clone();
|
|
let path = base_path.clone();
|
|
let handle = tokio::spawn(async move {
|
|
let config = KmsConfig::local(path).with_insecure_development_defaults();
|
|
manager_clone.reconfigure(config).await
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Wait for all reconfigurations to complete
|
|
let mut results = Vec::new();
|
|
for handle in handles {
|
|
results.push(handle.await);
|
|
}
|
|
|
|
// All should succeed (serialized by mutex)
|
|
for result in results {
|
|
assert!(result.expect("Task should complete").is_ok());
|
|
}
|
|
|
|
// Final version should be 6 (1 initial + 5 reconfigurations)
|
|
let final_version = manager.get_service_version().await.expect("Service should have version");
|
|
assert_eq!(final_version, 6);
|
|
}
|
|
}
|