security(kms): require explicit dev defaults opt-in (#3369)

* security(kms): require explicit dev defaults opt-in

* test(kms): satisfy clippy dev defaults checks
This commit is contained in:
安正超
2026-06-12 07:05:20 +08:00
committed by GitHub
parent a85cc0354c
commit c3055f9335
18 changed files with 446 additions and 85 deletions
+61
View File
@@ -47,6 +47,8 @@ pub struct ConfigureLocalKmsRequest {
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 ConfigureLocalKmsRequest {
@@ -62,6 +64,7 @@ impl fmt::Debug for ConfigureLocalKmsRequest {
.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()
}
}
@@ -95,6 +98,8 @@ pub struct ConfigureVaultKmsRequest {
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>,
}
/// Request to configure KMS with Vault Transit backend
@@ -122,6 +127,8 @@ pub struct ConfigureVaultTransitKmsRequest {
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>,
}
/// Generic KMS configuration request
@@ -351,6 +358,7 @@ impl ConfigureLocalKmsRequest {
master_key: self.master_key.clone(),
file_permissions: self.file_permissions,
}),
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),
@@ -387,6 +395,7 @@ impl ConfigureVaultKmsRequest {
None
},
})),
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),
@@ -421,6 +430,7 @@ impl ConfigureVaultTransitKmsRequest {
None
},
})),
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),
@@ -509,6 +519,53 @@ mod tests {
assert_eq!(config.backend, KmsBackend::Local);
}
#[test]
fn test_configure_request_development_defaults_require_opt_in() {
let local_raw = serde_json::json!({
"backend_type": "local",
"key_dir": "/tmp/kms-key-dir"
});
let request: ConfigureKmsRequest = serde_json::from_value(local_raw).expect("local request should deserialize");
let config = request.to_kms_config();
assert!(config.validate().is_err());
let local_opt_in_raw = serde_json::json!({
"backend_type": "local",
"key_dir": "/tmp/kms-key-dir",
"allow_insecure_dev_defaults": true
});
let request: ConfigureKmsRequest = serde_json::from_value(local_opt_in_raw).expect("local request should deserialize");
assert!(request.to_kms_config().validate().is_ok());
let vault_raw = serde_json::json!({
"backend_type": "vault",
"address": "http://127.0.0.1:8200",
"auth_method": {
"Token": {
"token": "dev-token"
}
},
"skip_tls_verify": true
});
let request: ConfigureKmsRequest = serde_json::from_value(vault_raw).expect("vault request should deserialize");
let config = request.to_kms_config();
assert!(config.validate().is_err());
let vault_opt_in_raw = serde_json::json!({
"backend_type": "vault",
"address": "http://127.0.0.1:8200",
"auth_method": {
"Token": {
"token": "dev-token"
}
},
"skip_tls_verify": true,
"allow_insecure_dev_defaults": true
});
let request: ConfigureKmsRequest = serde_json::from_value(vault_opt_in_raw).expect("vault request should deserialize");
assert!(request.to_kms_config().validate().is_ok());
}
#[test]
fn test_vault_transit_summary_reports_backend_details() {
let config = KmsConfig {
@@ -523,6 +580,7 @@ mod tests {
mount_path: "transit".to_string(),
tls: None,
})),
allow_insecure_dev_defaults: true,
timeout: Duration::from_secs(30),
retry_attempts: 3,
enable_cache: true,
@@ -569,6 +627,7 @@ mod tests {
enable_cache: Some(true),
max_cached_keys: Some(16),
cache_ttl_seconds: Some(60),
allow_insecure_dev_defaults: None,
});
let vault = ConfigureKmsRequest::VaultTransit(ConfigureVaultTransitKmsRequest {
address: "https://vault.example.com:8200".to_string(),
@@ -584,6 +643,7 @@ mod tests {
enable_cache: None,
max_cached_keys: None,
cache_ttl_seconds: None,
allow_insecure_dev_defaults: None,
});
let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest {
address: "https://vault.example.com:8200".to_string(),
@@ -602,6 +662,7 @@ mod tests {
enable_cache: None,
max_cached_keys: None,
cache_ttl_seconds: None,
allow_insecure_dev_defaults: None,
});
let rendered = format!("{local:?}\n{vault:?}\n{approle:?}");
+2
View File
@@ -564,6 +564,8 @@ pub struct LocalKmsBackend {
impl LocalKmsBackend {
/// Create a new LocalKmsBackend
pub async fn new(config: KmsConfig) -> Result<Self> {
config.validate()?;
let local_config = match &config.backend_config {
crate::config::BackendConfig::Local(local_config) => local_config.clone(),
crate::config::BackendConfig::VaultKv2(_) | crate::config::BackendConfig::VaultTransit(_) => {
+2
View File
@@ -595,6 +595,8 @@ pub struct VaultKmsBackend {
impl VaultKmsBackend {
/// Create a new VaultKmsBackend
pub async fn new(config: KmsConfig) -> Result<Self> {
config.validate()?;
let vault_config = match &config.backend_config {
crate::config::BackendConfig::VaultKv2(vault_config) => (**vault_config).clone(),
crate::config::BackendConfig::Local(_) | crate::config::BackendConfig::VaultTransit(_) => {
+2
View File
@@ -483,6 +483,8 @@ pub struct VaultTransitKmsBackend {
impl VaultTransitKmsBackend {
pub async fn new(config: KmsConfig) -> Result<Self> {
config.validate()?;
let vault_config = match &config.backend_config {
crate::config::BackendConfig::VaultTransit(vault_config) => (**vault_config).clone(),
crate::config::BackendConfig::VaultKv2(vault_config) => VaultTransitConfig {
+199 -6
View File
@@ -19,10 +19,13 @@ use rustfs_security_governance::{RedactionLevel, RedactionRule};
use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::time::Duration;
use url::Url;
pub const ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS: &str = "RUSTFS_KMS_ALLOW_INSECURE_DEV_DEFAULTS";
pub const ENV_KMS_VAULT_SKIP_TLS_VERIFY: &str = "RUSTFS_KMS_VAULT_SKIP_TLS_VERIFY";
pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
RedactionRule::new("kms.local.master_key", RedactionLevel::Secret, "local backend key encryption material"),
RedactionRule::new("kms.vault.token", RedactionLevel::Secret, "vault authentication token"),
@@ -93,6 +96,9 @@ pub struct KmsConfig {
pub default_key_id: Option<String>,
/// Backend-specific configuration
pub backend_config: BackendConfig,
/// Allow development-only insecure defaults such as plaintext local keys or HTTP Vault.
#[serde(default)]
pub allow_insecure_dev_defaults: bool,
/// Operation timeout
pub timeout: Duration,
/// Number of retry attempts
@@ -109,6 +115,7 @@ impl Default for KmsConfig {
backend: KmsBackend::default(),
default_key_id: None,
backend_config: BackendConfig::default(),
allow_insecure_dev_defaults: false,
timeout: Duration::from_secs(30),
retry_attempts: 3,
enable_cache: true,
@@ -405,6 +412,12 @@ impl KmsConfig {
self
}
/// Explicitly allow development-only KMS defaults.
pub fn with_insecure_development_defaults(mut self) -> Self {
self.allow_insecure_dev_defaults = true;
self
}
/// Set operation timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
@@ -435,12 +448,30 @@ impl KmsConfig {
if !config.key_dir.is_absolute() {
return Err(KmsError::configuration_error("Local key directory must be an absolute path"));
}
if !self.allow_insecure_dev_defaults {
if config.master_key.as_deref().is_none_or(str::is_empty) {
return Err(development_default_error(
"Local KMS requires a master key outside explicit development mode",
));
}
if is_under_temp_dir(&config.key_dir) {
return Err(development_default_error(
"Local KMS key directory must not be under the process temp directory outside explicit development mode",
));
}
}
}
BackendConfig::VaultKv2(config) => {
if !config.address.starts_with("http://") && !config.address.starts_with("https://") {
return Err(KmsError::configuration_error("Vault KV2 address must use http or https scheme"));
}
if !self.allow_insecure_dev_defaults {
validate_vault_development_defaults("Vault KV2", &config.address, &config.auth_method, config.tls.as_ref())?;
}
if config.mount_path.is_empty() {
return Err(KmsError::configuration_error("Vault KV2 mount path cannot be empty"));
}
@@ -461,6 +492,15 @@ impl KmsConfig {
return Err(KmsError::configuration_error("Vault Transit address must use http or https scheme"));
}
if !self.allow_insecure_dev_defaults {
validate_vault_development_defaults(
"Vault Transit",
&config.address,
&config.auth_method,
config.tls.as_ref(),
)?;
}
if config.mount_path.is_empty() {
return Err(KmsError::configuration_error("Vault Transit mount path cannot be empty"));
}
@@ -520,6 +560,8 @@ impl KmsConfig {
// Enable cache
config.enable_cache = get_env_bool("RUSTFS_KMS_ENABLE_CACHE", config.enable_cache);
config.allow_insecure_dev_defaults =
get_env_bool(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, config.allow_insecure_dev_defaults);
// Backend-specific configuration
match config.backend {
@@ -536,6 +578,7 @@ impl KmsConfig {
KmsBackend::VaultKv2 => {
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token");
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig {
address,
@@ -544,19 +587,20 @@ impl KmsConfig {
mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"),
kv_mount: get_env_str("RUSTFS_KMS_VAULT_KV_MOUNT", "secret"),
key_path_prefix: get_env_str("RUSTFS_KMS_VAULT_KEY_PREFIX", "rustfs/kms/keys"),
tls: None,
tls: vault_tls_config(skip_tls_verify),
}));
}
KmsBackend::VaultTransit => {
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token");
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
config.backend_config = BackendConfig::VaultTransit(Box::new(VaultTransitConfig {
address,
auth_method: VaultAuthMethod::Token { token },
namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"),
mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"),
tls: None,
tls: vault_tls_config(skip_tls_verify),
}));
}
}
@@ -566,6 +610,50 @@ impl KmsConfig {
}
}
fn vault_tls_config(skip_tls_verify: bool) -> Option<TlsConfig> {
skip_tls_verify.then_some(TlsConfig {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
skip_verify: true,
})
}
fn development_default_error(reason: &str) -> KmsError {
KmsError::configuration_error(format!("{reason}; set {ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS}=true only for development"))
}
fn is_under_temp_dir(path: &Path) -> bool {
path.starts_with(std::env::temp_dir())
}
fn validate_vault_development_defaults(
backend_name: &str,
address: &str,
auth_method: &VaultAuthMethod,
tls: Option<&TlsConfig>,
) -> Result<()> {
if address.starts_with("http://") {
return Err(development_default_error(&format!(
"{backend_name} requires HTTPS outside explicit development mode"
)));
}
if matches!(auth_method, VaultAuthMethod::Token { token } if token == "dev-token") {
return Err(development_default_error(&format!(
"{backend_name} default dev-token is not allowed outside explicit development mode"
)));
}
if tls.is_some_and(|tls| tls.skip_verify) {
return Err(development_default_error(&format!(
"{backend_name} skip TLS verification is not allowed outside explicit development mode"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -577,13 +665,14 @@ mod tests {
fn test_default_config() {
let config = KmsConfig::default();
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_ok());
assert!(config.validate().is_err());
assert!(config.with_insecure_development_defaults().validate().is_ok());
}
#[test]
fn test_local_config() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults();
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_ok());
@@ -592,6 +681,27 @@ mod tests {
assert_eq!(local_config.key_dir, temp_dir.path());
}
#[test]
fn test_local_development_defaults_require_opt_in() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
assert!(config.validate().is_err());
assert!(config.with_insecure_development_defaults().validate().is_ok());
let production_config = KmsConfig {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from("/var/lib/rustfs/kms"),
master_key: Some("production-master-key".to_string()),
file_permissions: Some(0o600),
}),
..Default::default()
};
assert!(production_config.validate().is_ok());
}
#[test]
fn test_vault_config() {
let address = Url::parse("https://vault.example.com:8200").expect("Valid URL");
@@ -617,6 +727,42 @@ mod tests {
assert_eq!(vault_config.mount_path, "transit");
}
#[test]
fn test_vault_development_defaults_require_opt_in() {
let http_address = Url::parse("http://127.0.0.1:8200").expect("Valid URL");
let https_address = Url::parse("https://vault.example.com:8200").expect("Valid URL");
let http_config = KmsConfig::vault(http_address, "vault-token".to_string());
assert!(http_config.validate().is_err());
assert!(http_config.with_insecure_development_defaults().validate().is_ok());
let dev_token_config = KmsConfig::vault(https_address.clone(), "dev-token".to_string());
assert!(dev_token_config.validate().is_err());
assert!(dev_token_config.with_insecure_development_defaults().validate().is_ok());
let skip_tls_config = KmsConfig {
backend: KmsBackend::VaultTransit,
backend_config: BackendConfig::VaultTransit(Box::new(VaultTransitConfig {
address: https_address.to_string(),
auth_method: VaultAuthMethod::Token {
token: "vault-token".to_string(),
},
namespace: None,
mount_path: "transit".to_string(),
tls: Some(TlsConfig {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
skip_verify: true,
}),
})),
..Default::default()
};
assert!(skip_tls_config.validate().is_err());
assert!(skip_tls_config.with_insecure_development_defaults().validate().is_ok());
}
#[test]
fn test_vault_kv2_backend_serialization_uses_pascal_case() {
let serialized = serde_json::to_string(&KmsBackend::VaultKv2).expect("backend should serialize");
@@ -710,7 +856,10 @@ mod tests {
#[test]
fn test_config_validation() {
let mut config = KmsConfig::default();
let mut config = KmsConfig {
allow_insecure_dev_defaults: true,
..Default::default()
};
// Valid config
assert!(config.validate().is_ok());
@@ -760,6 +909,50 @@ mod tests {
);
}
#[test]
fn test_from_env_requires_vault_development_opt_in() {
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")),
("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")),
],
|| {
let error = KmsConfig::from_env().expect_err("vault dev defaults should fail closed");
assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS));
},
);
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("http://127.0.0.1:8200")),
("RUSTFS_KMS_VAULT_TOKEN", Some("dev-token")),
(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS, Some("true")),
],
|| {
let config = KmsConfig::from_env().expect("explicit development opt-in should allow vault dev defaults");
assert!(config.allow_insecure_dev_defaults);
},
);
}
#[test]
fn test_from_env_rejects_vault_skip_tls_verify_without_opt_in() {
with_vars(
vec![
("RUSTFS_KMS_BACKEND", Some("vault-transit")),
("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")),
("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")),
(ENV_KMS_VAULT_SKIP_TLS_VERIFY, Some("true")),
],
|| {
let error = KmsConfig::from_env().expect_err("skip TLS verify should fail closed");
assert!(error.to_string().contains(ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS));
},
);
}
#[test]
fn test_from_env_reads_vault_transit_settings() {
with_vars(
+6 -6
View File
@@ -53,7 +53,7 @@
//! let service_manager = init_global_kms_service_manager();
//!
//! // Configure with local backend
//! let config = KmsConfig::local(PathBuf::from("./kms_keys"));
//! let config = KmsConfig::local(PathBuf::from("./kms_keys")).with_insecure_development_defaults();
//! service_manager.configure(config).await?;
//!
//! // Start the KMS service
@@ -135,7 +135,7 @@ mod tests {
// 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());
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");
@@ -160,7 +160,7 @@ mod tests {
// Start first service
let temp_dir1 = TempDir::new().expect("Failed to create temp dir");
let config1 = KmsConfig::local(temp_dir1.path().to_path_buf());
let config1 = KmsConfig::local(temp_dir1.path().to_path_buf()).with_insecure_development_defaults();
manager
.configure(config1.clone())
.await
@@ -176,7 +176,7 @@ mod tests {
// Reconfigure to new service (zero-downtime)
let temp_dir2 = TempDir::new().expect("Failed to create temp dir");
let config2 = KmsConfig::local(temp_dir2.path().to_path_buf());
let config2 = KmsConfig::local(temp_dir2.path().to_path_buf()).with_insecure_development_defaults();
manager.reconfigure(config2).await.expect("Reconfiguration should succeed");
// Verify version 2
@@ -205,7 +205,7 @@ mod tests {
let base_path = temp_dir.path().to_path_buf();
// Initial configuration
let config1 = KmsConfig::local(base_path.clone());
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");
@@ -215,7 +215,7 @@ mod tests {
let manager_clone = manager.clone();
let path = base_path.clone();
let handle = tokio::spawn(async move {
let config = KmsConfig::local(path);
let config = KmsConfig::local(path).with_insecure_development_defaults();
manager_clone.reconfigure(config).await
});
handles.push(handle);
+2 -2
View File
@@ -166,7 +166,7 @@ mod tests {
#[tokio::test]
async fn test_manager_operations() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults();
let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend"));
let manager = KmsManager::new(backend, config);
@@ -216,7 +216,7 @@ mod tests {
#[tokio::test]
async fn generate_data_key_does_not_reuse_context_bound_ciphertext() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults();
let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend"));
let manager = KmsManager::new(backend, config);
+3 -1
View File
@@ -738,7 +738,9 @@ mod tests {
async fn create_test_service() -> (ObjectEncryptionService, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_default_key("test-key".to_string());
let config = KmsConfig::local(temp_dir.path().to_path_buf())
.with_insecure_development_defaults()
.with_default_key("test-key".to_string());
let backend = Arc::new(
crate::backends::local::LocalKmsBackend::new(config.clone())
.await
+24
View File
@@ -92,6 +92,8 @@ impl KmsServiceManager {
/// Configure KMS with new configuration
pub async fn configure(&self, new_config: KmsConfig) -> Result<()> {
new_config.validate()?;
// Update configuration
{
let mut config = self.config.write().await;
@@ -200,6 +202,7 @@ impl KmsServiceManager {
let _guard = self.lifecycle_mutex.lock().await;
info!("Reconfiguring KMS service (zero-downtime)");
new_config.validate()?;
// Configure with new config
{
@@ -307,6 +310,8 @@ impl KmsServiceManager {
///
/// This creates a new backend, manager, and service, and assigns it a new version number.
async fn create_service_version(&self, config: &KmsConfig) -> Result<ServiceVersion> {
config.validate()?;
// Increment version counter
let version = self.version_counter.fetch_add(1, Ordering::Relaxed) + 1;
@@ -371,3 +376,22 @@ pub async fn get_global_encryption_service() -> Option<Arc<ObjectEncryptionServi
let manager = get_global_kms_service_manager().unwrap_or_else(init_global_kms_service_manager);
manager.get_encryption_service().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn configure_rejects_insecure_development_defaults_before_state_update() {
let manager = KmsServiceManager::new();
let error = manager
.configure(KmsConfig::default())
.await
.expect_err("unsafe local defaults should fail validation");
assert!(error.to_string().contains(crate::config::ENV_KMS_ALLOW_INSECURE_DEV_DEFAULTS));
assert_eq!(manager.get_status().await, KmsServiceStatus::NotConfigured);
assert!(manager.get_config().await.is_none());
}
}