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
+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")
);
});
}
}