feat(kms): accept the AWS backend through KMS configuration (#5592)

* feat(kms): accept the AWS backend through KMS configuration

The AWS KMS backend could be constructed but not selected: the admin
configure API had no AWS variant and startup rejected the backend name.

The configure request pins the region rather than defaulting it, because
that configuration is persisted once and replayed on every node: leaving
the region to each node's ambient provider chain would let nodes address
different regions, and therefore different keys, while reporting an
identical configuration. The request accepts no credential fields, so
credentials stay with the aws-config provider chain on each node, and
`deny_unknown_fields` refuses attempts to submit them anyway.

* test(kms): cover AWS backend selection through the service manager

An end-to-end check that an admin configure request selects the AWS
backend, builds a client, and passes the startup health check. Marked
#[ignore]: it needs real AWS credentials, though it creates no key and
is therefore not billable on its own.
This commit is contained in:
Zhengchao An
2026-08-02 03:58:20 +08:00
committed by GitHub
parent 105af08a10
commit 7528a0b916
7 changed files with 391 additions and 12 deletions
+189 -3
View File
@@ -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<bool>,
}
/// 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<String>,
/// Default master key ID for auto-encryption, as an AWS key id or ARN
pub default_key_id: Option<String>,
/// Operation timeout in seconds
pub timeout_seconds: Option<u64>,
/// Number of retry attempts
pub retry_attempts: Option<u32>,
/// Enable caching
pub enable_cache: Option<bool>,
/// Maximum number of keys to cache
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 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::<ConfigureKmsRequest>(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::<ConfigureKmsRequest>(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::<ConfigureKmsRequest>(raw).expect_err("unknown aws field should fail");
assert!(err.to_string().contains("unknown field"));
}
#[test]
+3 -3
View File
@@ -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};
+36
View File
@@ -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();
+3 -1
View File
@@ -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
+73 -3
View File
@@ -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 {
+1 -1
View File
@@ -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,
+86 -1
View File
@@ -430,6 +430,37 @@ fn build_static_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::
Ok(kms_config)
}
/// Build KMS configuration for the AWS KMS backend
///
/// No credential material is read here: AWS credentials are resolved by the
/// standard `aws-config` provider chain (environment, shared profile,
/// container/IMDS role), so only the two non-credential settings are taken
/// from the environment. An unresolvable region fails the backend closed when
/// the service starts.
fn build_aws_kms_config(cfg: &config::Config) -> std::io::Result<rustfs_kms::config::KmsConfig> {
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<rustfs_kms::KmsServiceManager>,
@@ -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<Option<ShutdownHandle>, Box<dyn std::e
#[cfg(test)]
mod tests {
use super::{notification_config_to_event_rules, resolve_buffer_profile_config};
use super::{build_aws_kms_config, notification_config_to_event_rules, resolve_buffer_profile_config};
use crate::config::{BufferConfig, WorkloadProfile};
use rustfs_config::KI_B;
use rustfs_s3_types::EventName;
@@ -1466,4 +1498,57 @@ mod tests {
assert!(err.to_string().contains("Invalid ARN"), "unexpected error: {err}");
}
fn aws_kms_test_config() -> 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")
);
});
}
}