fix(kms): honour the configured metadata cache TTL and metrics switch (#5569)

* fix(kms): honour the configured metadata cache TTL and metrics switch

KmsManager::new built the KmsCache from cache_config.max_keys alone, so
cache_config.ttl was dead configuration: every deployment ran the
hardcoded 300s window whatever the admin configure API was given, while
CacheSummary and the KMS config endpoint reported the configured value
back. cache_config.enable_metrics was never read anywhere.

Build the cache from the whole CacheConfig. The documented default is
reconciled down to the 300s the cache has always used rather than up to
the advertised 3600s, and now lives in one place (DEFAULT_CACHE_TTL)
instead of being duplicated across the four configure-request
converters, so the default path behaves exactly as before.

Behaviour change: a deployment configured through the admin API already
has ttl 3600 persisted, because the old converters wrote that default
into the stored config, so its describe_key staleness window widens from
an effective 300s to the 3600s it asked for. No cryptographic or
authorization path widens - encrypt, decrypt and generate_data_key go
straight to the backend and never read this cache. The Vault Transit
backend's own metadata cache, which does gate crypto through
ensure_key_state_allows, stays fixed at 300s and is now documented as
deliberately not operator-tunable.

The configured duration now reaches moka's builder, which panics above
1000 years, so CacheConfig::effective_ttl clamps to a 24h maximum the
way effective_timeout already clamps its own, and validate rejects a
zero TTL beside the existing max_keys check. Both config summaries and
the KMS config endpoint report the effective value, so the admin API
cannot advertise a lifetime the cache does not honour.

enable_metrics gates publication of the rustfs_kms_metadata_cache_*
families only; the counters behind the admin status API keep running
either way. No configure-request field sets it yet.

Refs rustfs/backlog#1584

* docs(kms): state why the Transit metadata TTL is not bound to the default

The comment claimed the constant matches config::DEFAULT_CACHE_TTL, which
reads as an invariant the code does not enforce. Say plainly that the
equality is a coincidence rather than a contract, and why binding the two
would be wrong: this cache gates crypto through ensure_key_state_allows,
so a later change to the operator-facing describe-cache default must not
be able to widen its staleness window.
This commit is contained in:
Zhengchao An
2026-08-01 21:27:33 +08:00
committed by GitHub
parent 29dedcc7fd
commit ad721bff42
7 changed files with 257 additions and 58 deletions
+41 -1
View File
@@ -46,7 +46,7 @@ pub struct KmsManager {
impl KmsManager {
/// Create a new KMS manager with the given backend and config
pub fn new(backend: Arc<dyn KmsBackend>, config: KmsConfig) -> Self {
let cache = Arc::new(RwLock::new(KmsCache::new(config.cache_config.max_keys as u64)));
let cache = Arc::new(RwLock::new(KmsCache::new(&config.cache_config)));
if config.allow_immediate_deletion {
warn!(
"KMS immediate key deletion is enabled: a DeleteKey request may destroy key material without any waiting window, and every object encrypted under that key becomes permanently unreadable"
@@ -462,6 +462,7 @@ mod tests {
use jiff::Zoned;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
use tempfile::tempdir;
/// Sink that keeps every record so tests can assert on the audit trail.
@@ -971,6 +972,45 @@ mod tests {
assert!(health);
}
#[tokio::test]
async fn configured_cache_ttl_bounds_how_long_metadata_is_reused() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let mut config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults();
config.cache_config.ttl = Duration::from_millis(100);
let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend"));
let manager = KmsManager::new(backend, config);
let key_id = manager
.create_key(CreateKeyRequest {
key_name: Some("cache-ttl-wiring".to_string()),
..Default::default()
})
.await
.expect("Failed to create key")
.key_id;
// Creating the key populated the cache, so this describe is served from it.
manager
.describe_key(DescribeKeyRequest { key_id: key_id.clone() })
.await
.expect("describe should succeed");
assert_eq!(manager.cache_stats().await.expect("cache is enabled").hits, 1);
// Past the configured lifetime the entry is gone and the describe falls
// through to the backend. A cache built with a hardcoded lifetime would
// still be serving the entry here.
tokio::time::sleep(Duration::from_millis(150)).await;
manager
.describe_key(DescribeKeyRequest { key_id })
.await
.expect("describe should succeed");
let stats = manager.cache_stats().await.expect("cache is enabled");
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn lifecycle_round_trip_invalidates_cached_metadata() {
let temp_dir = tempdir().expect("Failed to create temp dir");