diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index 80a7ee458..93f477e26 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -15,9 +15,10 @@ //! API types for KMS dynamic configuration use crate::config::{ - BackendConfig, CacheConfig, DEFAULT_CACHE_TTL, DEFAULT_MAX_CACHED_KEYS, DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, - DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, KmsBackend, KmsConfig, LocalConfig, StaticConfig, TlsConfig, VaultAuthMethod, - VaultConfig, VaultTransitConfig, allow_immediate_deletion_from_env, redacted_secret, redacted_secret_option, + AwsKmsConfig, BackendConfig, CacheConfig, DEFAULT_CACHE_TTL, DEFAULT_MAX_CACHED_KEYS, + DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX, DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT, KmsBackend, KmsConfig, LocalConfig, + StaticConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig, allow_immediate_deletion_from_env, + redacted_secret, redacted_secret_option, }; use crate::service_manager::KmsServiceStatus; use crate::types::{KeyMetadata, KeyUsage}; @@ -165,6 +166,47 @@ pub struct ConfigureStaticKmsRequest { pub allow_insecure_dev_defaults: Option, } +/// Request to configure KMS with the AWS KMS backend. +/// +/// Accepts no credential material by design: every node resolves AWS +/// credentials through the standard `aws-config` provider chain, so nothing +/// secret is submitted here, persisted with the cluster configuration, or +/// echoed back by the status API. +/// +/// Keys are not created by this path. The backend refuses caller-named key +/// creation because AWS assigns identifiers itself, so `default_key_id` must +/// name a key that already exists in AWS, by key id or ARN. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigureAwsKmsRequest { + /// AWS region hosting the keys. + /// + /// Mandatory here, unlike the environment-variable path: this + /// configuration is persisted once and replayed on every node, so leaving + /// the region to each node's ambient provider chain would let nodes + /// silently address different regions — and therefore different keys — + /// while reporting the same configuration. + pub region: String, + /// Endpoint override for local emulators and private endpoints. Unset in + /// production, where the SDK derives the regional endpoint; a plaintext + /// endpoint stays gated behind the development opt-in. + pub endpoint_url: Option, + /// Default master key ID for auto-encryption, as an AWS key id or ARN + pub default_key_id: Option, + /// Operation timeout in seconds + pub timeout_seconds: Option, + /// Number of retry attempts + pub retry_attempts: Option, + /// Enable caching + pub enable_cache: Option, + /// Maximum number of keys to cache + pub max_cached_keys: Option, + /// Cache TTL in seconds + pub cache_ttl_seconds: Option, + /// Allow development-only insecure defaults + pub allow_insecure_dev_defaults: Option, +} + impl Drop for ConfigureStaticKmsRequest { fn drop(&mut self) { use zeroize::Zeroize; @@ -211,6 +253,9 @@ pub enum ConfigureKmsRequest { /// Configure with Static single-key backend #[serde(rename = "Static", alias = "static")] Static(ConfigureStaticKmsRequest), + /// Configure with the AWS KMS backend + #[serde(rename = "AWS", alias = "AwsKms", alias = "aws", alias = "aws-kms", alias = "aws_kms")] + Aws(ConfigureAwsKmsRequest), } /// KMS configuration response @@ -636,6 +681,33 @@ impl ConfigureStaticKmsRequest { } } +impl ConfigureAwsKmsRequest { + /// Convert to KmsConfig + pub fn to_kms_config(&self) -> KmsConfig { + KmsConfig { + backend: KmsBackend::Aws, + default_key_id: self.default_key_id.clone(), + backend_config: BackendConfig::Aws(Box::new(AwsKmsConfig { + region: Some(self.region.clone()), + endpoint_url: self.endpoint_url.clone(), + })), + allow_insecure_dev_defaults: self.allow_insecure_dev_defaults.unwrap_or(false), + // Read from server configuration, never from the request body: the + // gate must mean the same thing whether KMS was configured at + // startup or through this endpoint. + allow_immediate_deletion: allow_immediate_deletion_from_env(), + 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(DEFAULT_MAX_CACHED_KEYS), + ttl: self.cache_ttl_seconds.map_or(DEFAULT_CACHE_TTL, Duration::from_secs), + ..CacheConfig::default() + }, + } + } +} + impl ConfigureKmsRequest { /// Convert to KmsConfig pub fn to_kms_config(&self) -> KmsConfig { @@ -644,6 +716,7 @@ impl ConfigureKmsRequest { ConfigureKmsRequest::VaultKv2(req) => req.to_kms_config(), ConfigureKmsRequest::VaultTransit(req) => req.to_kms_config(), ConfigureKmsRequest::Static(req) => req.to_kms_config(), + ConfigureKmsRequest::Aws(req) => req.to_kms_config(), } } } @@ -829,6 +902,108 @@ mod tests { assert!(request.to_kms_config().validate().is_ok()); } + #[test] + fn test_deserialize_aws_configure_request_accepts_type_aliases() { + for backend_type in ["AWS", "AwsKms", "aws", "aws-kms", "aws_kms"] { + let raw = serde_json::json!({ + "backend_type": backend_type, + "region": "eu-central-1", + "default_key_id": "arn:aws:kms:eu-central-1:111122223333:key/1234abcd" + }); + + let request: ConfigureKmsRequest = serde_json::from_value(raw).unwrap_or_else(|e| panic!("{backend_type}: {e}")); + let config = request.to_kms_config(); + assert_eq!(config.backend, KmsBackend::Aws, "backend_type={backend_type}"); + let aws = config.aws_kms_config().expect("aws backend config"); + assert_eq!(aws.region.as_deref(), Some("eu-central-1")); + assert_eq!(aws.endpoint_url, None); + assert!(config.validate().is_ok(), "backend_type={backend_type}"); + } + } + + /// A cluster-persisted AWS configuration must pin its own region: a + /// request that leaves it to each node's ambient provider chain is refused + /// rather than accepted into a configuration every node interprets + /// differently. + #[test] + fn test_aws_configure_request_requires_an_explicit_region() { + let missing = serde_json::json!({ + "backend_type": "AWS", + "default_key_id": "arn:aws:kms:eu-central-1:111122223333:key/1234abcd" + }); + let err = serde_json::from_value::(missing).expect_err("a region-less AWS request must be refused"); + assert!(err.to_string().contains("region"), "{err}"); + + let empty = serde_json::json!({ "backend_type": "AWS", "region": "" }); + let request: ConfigureKmsRequest = serde_json::from_value(empty).expect("an empty region deserializes"); + assert!(request.to_kms_config().validate().is_err(), "an empty region must not validate"); + } + + #[test] + fn test_aws_configure_request_rejects_plaintext_endpoint_without_opt_in() { + let raw = serde_json::json!({ + "backend_type": "AWS", + "region": "us-east-1", + "endpoint_url": "http://localhost:4566" + }); + let request: ConfigureKmsRequest = serde_json::from_value(raw).expect("aws request should deserialize"); + assert!(request.to_kms_config().validate().is_err()); + + let opt_in = serde_json::json!({ + "backend_type": "AWS", + "region": "us-east-1", + "endpoint_url": "http://localhost:4566", + "allow_insecure_dev_defaults": true + }); + let request: ConfigureKmsRequest = serde_json::from_value(opt_in).expect("aws request should deserialize"); + assert!(request.to_kms_config().validate().is_ok()); + } + + /// The AWS summary carries only non-credential settings, because the + /// backend never holds AWS credential material to begin with. + #[test] + fn test_aws_status_summary_reports_only_non_credential_settings() { + let config = ConfigureAwsKmsRequest { + region: "us-east-1".to_string(), + endpoint_url: None, + default_key_id: Some("arn:aws:kms:us-east-1:111122223333:key/1234abcd".to_string()), + timeout_seconds: None, + retry_attempts: None, + enable_cache: None, + max_cached_keys: None, + cache_ttl_seconds: None, + allow_insecure_dev_defaults: None, + } + .to_kms_config(); + + let summary = KmsConfigSummary::from(&config); + assert_eq!(summary.backend_type, KmsBackend::Aws); + match &summary.backend_summary { + BackendSummary::Aws { region, endpoint_url } => { + assert_eq!(region.as_deref(), Some("us-east-1")); + assert_eq!(endpoint_url.as_deref(), None); + } + other => panic!("expected aws summary, got {other:?}"), + } + + let response = KmsStatusResponse { + status: KmsServiceStatus::Running, + backend_type: Some(config.backend), + healthy: Some(true), + config_summary: Some(summary), + }; + let rendered = format!( + "{}\n{response:?}", + serde_json::to_string(&response).expect("kms status response should serialize") + ); + for credential_field in ["access_key", "secret_key", "session_token", "has_stored_credentials"] { + assert!( + !rendered.contains(credential_field), + "aws status output must not describe credential material: {rendered}" + ); + } + } + #[test] fn test_configure_request_rejects_unknown_fields() { let raw = serde_json::json!({ @@ -853,6 +1028,17 @@ mod tests { let err = serde_json::from_value::(raw).expect_err("unknown auth field should fail"); assert!(err.to_string().contains("unknown field")); + + // AWS credentials belong to the provider chain: a request that tries to + // smuggle them in must be refused, not silently ignored. + let raw = serde_json::json!({ + "backend_type": "AWS", + "region": "us-east-1", + "secret_access_key": "AKIA-not-accepted-here" + }); + + let err = serde_json::from_value::(raw).expect_err("unknown aws field should fail"); + assert!(err.to_string().contains("unknown field")); } #[test] diff --git a/crates/kms/src/lib.rs b/crates/kms/src/lib.rs index 26b0c9a2a..cd4f70e13 100644 --- a/crates/kms/src/lib.rs +++ b/crates/kms/src/lib.rs @@ -83,9 +83,9 @@ 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, + 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}; diff --git a/crates/kms/src/service_manager.rs b/crates/kms/src/service_manager.rs index 8e43e0f43..204478b63 100644 --- a/crates/kms/src/service_manager.rs +++ b/crates/kms/src/service_manager.rs @@ -749,6 +749,42 @@ mod tests { KmsConfig::static_kms(key_id.to_string(), BASE64_STANDARD.encode([fill; 32])) } + /// End-to-end wiring check for the AWS backend: an admin configure request + /// must select it, build a real client, and pass the startup health check. + /// + /// `RUSTFS_KMS_AWS_REGION` names the region; the credential chain supplies + /// the rest. No key is created, so this test is not billable on its own. + #[tokio::test] + #[ignore] // Requires real AWS credentials + async fn aws_backend_configure_and_start_end_to_end() { + let region = std::env::var("RUSTFS_KMS_AWS_REGION").expect("RUSTFS_KMS_AWS_REGION must name the test region"); + let config = crate::api_types::ConfigureAwsKmsRequest { + region, + endpoint_url: None, + default_key_id: std::env::var("RUSTFS_KMS_DEFAULT_KEY_ID").ok(), + timeout_seconds: None, + retry_attempts: None, + enable_cache: None, + max_cached_keys: None, + cache_ttl_seconds: None, + allow_insecure_dev_defaults: None, + } + .to_kms_config(); + + let manager = KmsServiceManager::new(); + manager.configure(config).await.expect("configure the AWS backend"); + manager.start().await.expect("start the AWS backend"); + + assert_eq!(manager.get_status().await, KmsServiceStatus::Running); + let capabilities = manager + .get_manager() + .await + .expect("a running service exposes its manager") + .backend_capabilities(); + assert!(capabilities.schedule_deletion); + assert!(!capabilities.physical_delete); + } + #[tokio::test] async fn configure_rejects_insecure_development_defaults_before_state_update() { let manager = KmsServiceManager::new(); diff --git a/docs/operations/kms-backend-security.md b/docs/operations/kms-backend-security.md index bbf6d4faa..32d7893d3 100644 --- a/docs/operations/kms-backend-security.md +++ b/docs/operations/kms-backend-security.md @@ -170,7 +170,9 @@ Two consequences follow from that last row: **SSE-S3 key auto-creation and the s Key versions are opaque. AWS addresses backing keys internally and picks the right one to decrypt with, so RustFS reports `key_version` as 1 and cannot enumerate versions. Rotation uses `RotateKeyOnDemand`, which retains prior backing keys for decryption; AWS's separate automatic yearly rotation is neither enabled nor reported on by RustFS. -The AWS backend is not configurable through the KMS admin API — use the environment variables above at startup. +The KMS admin API accepts the AWS backend as `"backend_type": "AWS"` (aliases `aws`, `aws-kms`, `aws_kms`, `AwsKms`) on `/v3/kms/configure` and `/v3/kms/reconfigure`. The body carries `region` (**required**), and optionally `endpoint_url`, `default_key_id`, and the shared timeout/retry/cache settings. It accepts no credential fields at all — unknown fields are rejected — because every node resolves credentials through its own provider chain. + +`region` is mandatory on this path even though `RUSTFS_KMS_AWS_REGION` is optional at startup: the admin configuration is persisted once and replayed on every node, so a request that left the region to each node's ambient chain would let nodes address different regions, and therefore different keys, while reporting an identical configuration. `default_key_id` must be an AWS key id or ARN that already exists — this backend never creates keys by name. ## Local backend durability and deployment support matrix diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index d8f9e565f..5a5ada5d0 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -129,6 +129,9 @@ fn normalize_configure_request_secrets( ConfigureKmsRequest::VaultTransit(req) => token_is_blank(&req.auth_method), ConfigureKmsRequest::Local(_) => false, ConfigureKmsRequest::Static(_) => false, + // AWS credentials come from the aws-config chain, so there is nothing + // to carry over from the existing configuration. + ConfigureKmsRequest::Aws(_) => false, }; if !needs_existing_auth { @@ -144,6 +147,7 @@ fn normalize_configure_request_secrets( ConfigureKmsRequest::VaultTransit(req) => req.auth_method = existing_auth, ConfigureKmsRequest::Local(_) => {} ConfigureKmsRequest::Static(_) => {} + ConfigureKmsRequest::Aws(_) => {} } Ok(()) @@ -1181,9 +1185,9 @@ impl Operation for ReconfigureKmsHandler { #[cfg(test)] mod tests { use super::{ - decode_persisted_kms_config, ensure_kms_config_persistable, kms_config_fingerprint, kms_config_is_unchanged, - kms_configure_actions, kms_service_control_actions, local_success_with_peer_report, normalize_configure_request_secrets, - redacted_canonical_config, + decode_persisted_kms_config, ensure_kms_config_persistable, ensure_kms_request_persistable, kms_config_fingerprint, + kms_config_is_unchanged, kms_configure_actions, kms_service_control_actions, local_success_with_peer_report, + normalize_configure_request_secrets, redacted_canonical_config, }; use rustfs_policy::policy::action::{Action, AdminAction, KmsAction}; use std::path::PathBuf; @@ -1420,6 +1424,72 @@ mod tests { assert_eq!(backend_error, "Changing from the Local KMS backend is not supported"); } + fn aws_configure_request(region: &str) -> rustfs_kms::ConfigureKmsRequest { + rustfs_kms::ConfigureKmsRequest::Aws(rustfs_kms::ConfigureAwsKmsRequest { + region: region.to_string(), + endpoint_url: None, + default_key_id: Some("arn:aws:kms:us-east-1:111122223333:key/1234abcd".to_string()), + timeout_seconds: None, + retry_attempts: None, + enable_cache: None, + max_cached_keys: None, + cache_ttl_seconds: None, + allow_insecure_dev_defaults: None, + }) + } + + /// The AWS backend holds no credential material of its own, so its + /// configuration is safe to persist cluster-wide and needs nothing carried + /// over from a previous configuration. + #[test] + fn aws_configure_request_is_persistable_and_needs_no_existing_credentials() { + let mut request = aws_configure_request("us-east-1"); + normalize_configure_request_secrets(&mut request, None).expect("aws request needs no existing credentials"); + assert!(ensure_kms_request_persistable(&request).is_ok()); + + let config = request.to_kms_config(); + assert!(ensure_kms_config_persistable(&config).is_ok()); + + let canonical = redacted_canonical_config(&config).expect("aws configuration should serialize"); + assert!(canonical.contains("us-east-1"), "the pinned region must drive the fingerprint"); + for credential_field in ["access_key", "secret_access_key", "session_token"] { + assert!( + !canonical.contains(credential_field), + "aws configuration must carry no credential material: {canonical}" + ); + } + } + + /// Two nodes cannot be allowed to read the same AWS configuration as + /// different regions, so the pinned region has to be part of what a + /// fingerprint comparison would flag as a split. + #[test] + fn aws_config_fingerprint_tracks_the_pinned_region() { + let first = kms_config_fingerprint(&aws_configure_request("us-east-1").to_kms_config()) + .expect("fingerprint should be computable"); + let same = kms_config_fingerprint(&aws_configure_request("us-east-1").to_kms_config()) + .expect("fingerprint should be computable"); + let other = kms_config_fingerprint(&aws_configure_request("eu-central-1").to_kms_config()) + .expect("fingerprint should be computable"); + + assert_eq!(first, same); + assert_ne!(first, other); + } + + #[test] + fn local_backend_cannot_be_switched_to_aws() { + let mut existing = rustfs_kms::KmsConfig::local(PathBuf::from("/var/lib/rustfs/kms")); + let rustfs_kms::BackendConfig::Local(existing_local) = &mut existing.backend_config else { + panic!("local constructor must create local backend config"); + }; + existing_local.master_key = Some("stored-master-key".to_string()); + + let mut request = aws_configure_request("us-east-1"); + let error = normalize_configure_request_secrets(&mut request, Some(&existing)) + .expect_err("switching away from the local backend must be rejected"); + assert_eq!(error, "Changing from the Local KMS backend is not supported"); + } + const VAULT_TOKEN: &str = "hvs-super-secret-token"; fn vault_config(address: &str, token: &str) -> rustfs_kms::KmsConfig { diff --git a/rustfs/src/config/cli.rs b/rustfs/src/config/cli.rs index 30332d128..93fc598cd 100644 --- a/rustfs/src/config/cli.rs +++ b/rustfs/src/config/cli.rs @@ -314,7 +314,7 @@ pub struct ServerOpts { #[arg(long, default_value_t = false, env = "RUSTFS_KMS_ENABLE")] pub kms_enable: bool, - /// KMS backend type: local, vault or vault-kv2 (plain Vault KV v2 storage), vault-transit + /// KMS backend type: local, vault or vault-kv2 (plain Vault KV v2 storage), vault-transit, static, aws #[arg(long, default_value_t = rustfs_config::DEFAULT_KMS_BACKEND.to_string(), env = "RUSTFS_KMS_BACKEND")] pub kms_backend: String, diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index fe5746582..67c889c1a 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -430,6 +430,37 @@ fn build_static_kms_config(cfg: &config::Config) -> std::io::Result std::io::Result { + use rustfs_kms::config::{AwsKmsConfig, ENV_KMS_AWS_ENDPOINT_URL, ENV_KMS_AWS_REGION}; + + let kms_config = rustfs_kms::config::KmsConfig { + backend: rustfs_kms::config::KmsBackend::Aws, + backend_config: rustfs_kms::config::BackendConfig::Aws(Box::new(AwsKmsConfig { + region: rustfs_utils::get_env_opt_str(ENV_KMS_AWS_REGION), + endpoint_url: rustfs_utils::get_env_opt_str(ENV_KMS_AWS_ENDPOINT_URL), + })), + allow_insecure_dev_defaults: cfg.kms_allow_insecure_dev_defaults, + allow_immediate_deletion: rustfs_kms::config::allow_immediate_deletion_from_env(), + // Keys are never auto-created on this backend: it refuses + // caller-named creation because AWS assigns identifiers, so the + // default key must already exist in AWS and be named by key id or ARN. + default_key_id: cfg.kms_default_key_id.clone(), + ..Default::default() + }; + + kms_config + .validate() + .map_err(|e| Error::other(format!("AWS KMS configuration validation failed: {e}")))?; + Ok(kms_config) +} + /// Configure and start KMS service async fn configure_and_start_kms( service_manager: &std::sync::Arc, @@ -506,6 +537,7 @@ pub async fn init_kms_system(config: &config::Config) -> std::io::Result<()> { "vault" | "vault-kv2" | "vault_kv2" => build_vault_kms_config(config)?, "vault-transit" | "vault_transit" => build_vault_transit_kms_config(config)?, "static" => build_static_kms_config(config)?, + "aws" | "aws-kms" | "aws_kms" => build_aws_kms_config(config)?, _ => return Err(Error::other(format!("Unsupported KMS backend: {}", config.kms_backend))), }; @@ -1373,7 +1405,7 @@ pub async fn init_sftp_system() -> Result, Box crate::config::Config { + let mut config = crate::config::Config::new("127.0.0.1:9000", vec!["/tmp/rustfs-aws-kms".to_string()]); + config.kms_enable = true; + config.kms_backend = "aws".to_string(); + config.kms_default_key_id = Some("arn:aws:kms:us-east-1:111122223333:key/1234abcd".to_string()); + config + } + + /// Startup takes only the two non-credential AWS settings from the + /// environment; credentials stay with the `aws-config` provider chain. + #[test] + fn build_aws_kms_config_reads_only_non_credential_settings() { + let config = temp_env::with_vars( + [ + ("RUSTFS_KMS_AWS_REGION", Some("eu-central-1")), + ("RUSTFS_KMS_AWS_ENDPOINT_URL", None), + ], + || build_aws_kms_config(&aws_kms_test_config()).expect("aws KMS configuration should build"), + ); + + assert_eq!(config.backend, rustfs_kms::config::KmsBackend::Aws); + let aws = config.aws_kms_config().expect("aws backend config"); + assert_eq!(aws.region.as_deref(), Some("eu-central-1")); + assert_eq!(aws.endpoint_url, None); + assert_eq!(config.default_key_id.as_deref(), Some("arn:aws:kms:us-east-1:111122223333:key/1234abcd")); + } + + /// A plaintext endpoint override exposes every KMS request, plaintext data + /// keys included, so startup refuses it without the development opt-in. + #[test] + fn build_aws_kms_config_refuses_a_plaintext_endpoint_without_opt_in() { + let vars = [ + ("RUSTFS_KMS_AWS_REGION", Some("us-east-1")), + ("RUSTFS_KMS_AWS_ENDPOINT_URL", Some("http://localhost:4566")), + ]; + + temp_env::with_vars(vars, || { + let error = + build_aws_kms_config(&aws_kms_test_config()).expect_err("a plaintext AWS endpoint must not start the server"); + assert!(error.to_string().contains("https"), "unexpected error: {error}"); + }); + + temp_env::with_vars(vars, || { + let mut config = aws_kms_test_config(); + config.kms_allow_insecure_dev_defaults = true; + let config = build_aws_kms_config(&config).expect("the development opt-in should accept a plaintext endpoint"); + assert_eq!( + config.aws_kms_config().expect("aws backend config").endpoint_url.as_deref(), + Some("http://localhost:4566") + ); + }); + } }