mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 07:36:53 +00:00
557a616ae6
* feat(kms): report configuration references that block a key deletion Adds a KeyImpactReport that states which configuration still points at a key, how exhaustively the sources were read, and which sources were not consulted at all. The report deliberately carries no in-use or safe-to-delete claim: it covers the configuration layer only, so an empty reference list means nothing was found in the scanned sources, never that the key is unreferenced. Immediate deletion destroys key material without ever reaching the deletion worker, so it never passed the worker's reference gate. The manager now consults the same checker on that path and refuses with a typed KeyStillReferenced error. This only ever adds a refusal; the scheduled deletion path and the worker's blocking behaviour are unchanged. * test(kms): cover the immediate-deletion reference refusal * feat(kms): surface configuration references on the admin key endpoints DeleteKey and DescribeKey now return an impact section listing the configuration that points at the key, so an operator scheduling a deletion sees what will refuse to destroy the material instead of learning it from a server-side log once the window has run out. The section is reported, never acted on: scheduling still succeeds while references exist, and the deletion worker's gate remains the only thing that decides whether material is destroyed. An immediate deletion that the manager refuses for an outstanding reference now answers 409. * test(kms): pin the impact wire shape and the unreferenced force-delete path * fix(kms): make the DescribeKey impact section opt-in Collecting the section lists every bucket, and DescribeKey is polled, so carrying that fan-out on the default read path trades a hot path's cost for a diagnostic. It is now collected only for impact=true; without the parameter the endpoint does exactly the work it did before and returns no impact field. A value that is neither true nor false is refused rather than read as off, so a typo cannot answer a request for the section with a response that merely lacks one. DeleteKey still reports unconditionally: that is the request whose consequences the caller cannot otherwise see, and it is not polled. * fix(kms): box the query-parse refusal now that responses carry impact The delete response grew an impact section, which pushed it past the size clippy accepts inline in a Result. It is a full response body rather than an error code, so it is boxed at the one place that returns it as an error; the wire shape and the public field type are unchanged.
253 lines
9.9 KiB
Rust
253 lines
9.9 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 key_impact;
|
|
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, ConfigureAwsKmsRequest, 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 key_impact::{KeyImpactReport, KeyReference, KeyReferenceKind, ReferenceCompleteness, ReferenceCoverage, ReferenceScope};
|
|
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);
|
|
}
|
|
}
|