From b1b3655bf8d0fbbaf3e95071c1c10aa9a850781d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Wed, 26 Aug 2026 13:25:49 +0800 Subject: [PATCH] feat(kms): surface non-production backend positioning at runtime (#6633) --- crates/kms/src/backends/aws.rs | 1 + crates/kms/src/backends/local.rs | 5 ++- crates/kms/src/backends/mod.rs | 39 +++++++++++++++++++ ..._aws__tests__aws_backend_capabilities.snap | 1 + ...ds__tests__local_backend_capabilities.snap | 1 + ...s__tests__static_backend_capabilities.snap | 1 + ...tests__vault_kv2_backend_capabilities.snap | 1 + ...s__vault_transit_backend_capabilities.snap | 1 + crates/kms/src/backends/static_kms.rs | 4 +- crates/kms/src/backends/vault.rs | 1 + crates/kms/src/backends/vault_transit.rs | 1 + crates/kms/src/config.rs | 24 +++++++++--- crates/kms/src/service_manager.rs | 14 +++++++ docs/operations/kms-backend-security.md | 6 +-- rustfs/src/config/cli.rs | 2 +- 15 files changed, 91 insertions(+), 11 deletions(-) diff --git a/crates/kms/src/backends/aws.rs b/crates/kms/src/backends/aws.rs index 4e8fc9c5b..93b85c956 100644 --- a/crates/kms/src/backends/aws.rs +++ b/crates/kms/src/backends/aws.rs @@ -821,6 +821,7 @@ impl KmsBackend for AwsKmsBackend { .with_schedule_deletion(true) .with_versioning(true) .with_physical_delete(false) + .with_production_supported(true) } /// Observe, never destroy. diff --git a/crates/kms/src/backends/local.rs b/crates/kms/src/backends/local.rs index 836c6d979..349c9bb0d 100644 --- a/crates/kms/src/backends/local.rs +++ b/crates/kms/src/backends/local.rs @@ -2279,7 +2279,10 @@ impl KmsBackend for LocalKmsBackend { fn capabilities(&self) -> BackendCapabilities { // Rotation stays unadvertised until historical key versions can be // retained (see LocalKmsClient::rotate_key); without version history - // there is also no versioning capability. + // there is also no versioning capability. `production_supported` stays + // false by positioning decision: the Local backend keeps its + // cryptographic root on the host filesystem and exists for + // development, testing and demos only. BackendCapabilities::minimal() .with_enable_disable(true) .with_schedule_deletion(true) diff --git a/crates/kms/src/backends/mod.rs b/crates/kms/src/backends/mod.rs index e3b8f0c1c..a6213724e 100644 --- a/crates/kms/src/backends/mod.rs +++ b/crates/kms/src/backends/mod.rs @@ -615,6 +615,14 @@ pub struct BackendCapabilities { pub update_key_metadata: bool, /// Re-wrapping an existing data key envelope onto the key's current version pub rewrap: bool, + /// Whether this backend is positioned for production use. + /// + /// Backends that keep their cryptographic root on the local host (Local, + /// Static) are for development, testing and demos only; this flag is how + /// that positioning reaches logs, the status API and the console without + /// each consumer matching on backend names. + #[serde(default)] + pub production_supported: bool, } impl BackendCapabilities { @@ -633,6 +641,7 @@ impl BackendCapabilities { physical_delete: false, update_key_metadata: false, rewrap: false, + production_supported: false, } } @@ -695,6 +704,12 @@ impl BackendCapabilities { self.rewrap = rewrap; self } + + /// Set whether the backend is positioned for production use + pub const fn with_production_supported(mut self, production_supported: bool) -> Self { + self.production_supported = production_supported; + self + } } impl Default for BackendCapabilities { @@ -775,6 +790,30 @@ mod tests { assert!(!capabilities.physical_delete); assert!(!capabilities.update_key_metadata); assert!(!capabilities.rewrap); + // Production positioning is an explicit claim, never inherited. + assert!(!capabilities.production_supported); + } + + /// Older peers and consoles serialize capabilities without the + /// positioning flag; deserializing their payloads must not fail and must + /// default to the conservative claim. + #[test] + fn capabilities_without_positioning_field_deserialize_as_non_production() { + let legacy = serde_json::json!({ + "encrypt": true, + "decrypt": true, + "generate_data_key": true, + "rotate": true, + "enable_disable": true, + "schedule_deletion": true, + "versioning": true, + "physical_delete": true, + "update_key_metadata": true, + "rewrap": true, + }); + let capabilities: BackendCapabilities = + serde_json::from_value(legacy).expect("legacy capability payloads must stay deserializable"); + assert!(!capabilities.production_supported); } #[tokio::test] diff --git a/crates/kms/src/backends/snapshots/rustfs_kms__backends__aws__tests__aws_backend_capabilities.snap b/crates/kms/src/backends/snapshots/rustfs_kms__backends__aws__tests__aws_backend_capabilities.snap index 91c0ac280..59b119950 100644 --- a/crates/kms/src/backends/snapshots/rustfs_kms__backends__aws__tests__aws_backend_capabilities.snap +++ b/crates/kms/src/backends/snapshots/rustfs_kms__backends__aws__tests__aws_backend_capabilities.snap @@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities()) "encrypt": true, "generate_data_key": true, "physical_delete": false, + "production_supported": true, "rewrap": false, "rotate": true, "schedule_deletion": true, diff --git a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__local_backend_capabilities.snap b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__local_backend_capabilities.snap index 0b73edf3b..9437bc902 100644 --- a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__local_backend_capabilities.snap +++ b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__local_backend_capabilities.snap @@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities()) "encrypt": true, "generate_data_key": true, "physical_delete": true, + "production_supported": false, "rewrap": false, "rotate": false, "schedule_deletion": true, diff --git a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__static_backend_capabilities.snap b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__static_backend_capabilities.snap index 50a4da278..75292999e 100644 --- a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__static_backend_capabilities.snap +++ b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__static_backend_capabilities.snap @@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities()) "encrypt": true, "generate_data_key": true, "physical_delete": false, + "production_supported": false, "rewrap": false, "rotate": false, "schedule_deletion": false, diff --git a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_kv2_backend_capabilities.snap b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_kv2_backend_capabilities.snap index 74a99a5b2..74999a0d5 100644 --- a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_kv2_backend_capabilities.snap +++ b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_kv2_backend_capabilities.snap @@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities()) "encrypt": true, "generate_data_key": true, "physical_delete": true, + "production_supported": true, "rewrap": true, "rotate": true, "schedule_deletion": true, diff --git a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_transit_backend_capabilities.snap b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_transit_backend_capabilities.snap index 74a99a5b2..74999a0d5 100644 --- a/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_transit_backend_capabilities.snap +++ b/crates/kms/src/backends/snapshots/rustfs_kms__backends__tests__vault_transit_backend_capabilities.snap @@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities()) "encrypt": true, "generate_data_key": true, "physical_delete": true, + "production_supported": true, "rewrap": true, "rotate": true, "schedule_deletion": true, diff --git a/crates/kms/src/backends/static_kms.rs b/crates/kms/src/backends/static_kms.rs index a4cc167e8..970d25da9 100644 --- a/crates/kms/src/backends/static_kms.rs +++ b/crates/kms/src/backends/static_kms.rs @@ -413,7 +413,9 @@ impl KmsBackend for StaticKmsBackend { fn capabilities(&self) -> BackendCapabilities { // Static KMS is a read-only single-key backend: it only performs - // cryptographic operations and rejects every lifecycle mutation. + // cryptographic operations and rejects every lifecycle mutation. Its + // key material comes straight from configuration, so like Local it is + // a development/testing backend and never production_supported. BackendCapabilities::minimal() } } diff --git a/crates/kms/src/backends/vault.rs b/crates/kms/src/backends/vault.rs index 56d2a856e..a430a269b 100644 --- a/crates/kms/src/backends/vault.rs +++ b/crates/kms/src/backends/vault.rs @@ -2257,6 +2257,7 @@ impl KmsBackend for VaultKmsBackend { .with_physical_delete(true) .with_update_key_metadata(true) .with_rewrap(true) + .with_production_supported(true) } async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result { diff --git a/crates/kms/src/backends/vault_transit.rs b/crates/kms/src/backends/vault_transit.rs index 602c53154..39bac2be1 100644 --- a/crates/kms/src/backends/vault_transit.rs +++ b/crates/kms/src/backends/vault_transit.rs @@ -1767,6 +1767,7 @@ impl KmsBackend for VaultTransitKmsBackend { .with_physical_delete(true) .with_update_key_metadata(true) .with_rewrap(true) + .with_production_supported(true) } async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result { diff --git a/crates/kms/src/config.rs b/crates/kms/src/config.rs index 6e8cc1d06..2f177af61 100644 --- a/crates/kms/src/config.rs +++ b/crates/kms/src/config.rs @@ -941,8 +941,15 @@ impl KmsConfig { && let Some(ref tls) = config.tls && !tls.skip_verify { - // In production, we should have proper TLS configuration - if tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() { + if tls.ca_cert_path.is_some() || tls.client_cert_path.is_some() || tls.client_key_path.is_some() { + // No configuration surface sets these paths today and the + // Vault client does not consume them; warn loudly instead + // of implying the certificates take effect. + tracing::warn!( + "Vault TLS certificate paths are configured but not applied to the Vault client; \ + the connection still relies on the system CA store without a client identity" + ); + } else { tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA"); } } @@ -978,10 +985,17 @@ impl KmsConfig { if config.address.starts_with("https://") && let Some(ref tls) = config.tls && !tls.skip_verify - && tls.ca_cert_path.is_none() - && tls.client_cert_path.is_none() { - tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA"); + if tls.ca_cert_path.is_some() || tls.client_cert_path.is_some() || tls.client_key_path.is_some() { + // Same as the KV2 branch: these paths are dead + // configuration until the client consumes them. + tracing::warn!( + "Vault TLS certificate paths are configured but not applied to the Vault client; \ + the connection still relies on the system CA store without a client identity" + ); + } else { + tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA"); + } } } BackendConfig::Static(config) => { diff --git a/crates/kms/src/service_manager.rs b/crates/kms/src/service_manager.rs index d30e6cb3a..ec67af821 100644 --- a/crates/kms/src/service_manager.rs +++ b/crates/kms/src/service_manager.rs @@ -633,6 +633,20 @@ impl KmsServiceManager { } }; + // Every path that can activate a backend (startup, persisted-config + // replay, dynamic configure, peer reload) converges here, so this is + // the one place a non-production backend is guaranteed to announce + // itself each time it starts serving. + if !backend.capabilities().production_supported { + warn!( + event = "kms_backend_positioning", + backend = config.backend.as_str(), + version, + "KMS backend is intended for development, testing and demos only; \ + use Vault Transit, Vault KV2 or AWS KMS for production deployments" + ); + } + // Create KMS manager // // The deletion reference checker is handed to the manager as well as to diff --git a/docs/operations/kms-backend-security.md b/docs/operations/kms-backend-security.md index 3f1dfdc7e..5135b3fb1 100644 --- a/docs/operations/kms-backend-security.md +++ b/docs/operations/kms-backend-security.md @@ -8,8 +8,8 @@ For how the Vault backends authenticate (static token, AppRole, Kubernetes, Vaul | Backend | Config tag | Master key material location | At-rest protection of key material | Durability | Rotation | Intended use | | --- | --- | --- | --- | --- | --- | --- | -| Local | `Local` | Files under `key_dir`, encrypted with the configured local master key | Local master key (AES-GCM) + file permissions | Crash-durable commits on local filesystems only; see [Local backend durability and deployment support matrix](#local-backend-durability-and-deployment-support-matrix) | Rejected by design (single material, development backend) | Development; single-node setups that accept host-level trust | -| Static | `Static` | Provided out-of-band via environment/file; never persisted by RustFS | Operator-managed secret distribution | No state persisted by RustFS | Rejected (read-only backend) | Simple deployments with an external secret manager | +| Local | `Local` | Files under `key_dir`, encrypted with the configured local master key | Local master key (AES-GCM) + file permissions | Crash-durable commits on local filesystems only; see [Local backend durability and deployment support matrix](#local-backend-durability-and-deployment-support-matrix) | Rejected by design (single material, development backend) | Development, testing and demos only; not supported for production | +| Static | `Static` | Provided out-of-band via environment/file; never persisted by RustFS | Operator-managed secret distribution | No state persisted by RustFS | Rejected (read-only backend) | Development and testing with an externally supplied key; not supported for production | | Vault KV2 | `VaultKV2` (legacy alias `Vault`) | Stored **directly** in Vault KV v2 (Base64-encoded plaintext) | Vault ACLs + KV v2 at-rest encryption + TLS only | Delegated to Vault storage | Versioned retention (immutable per-version records + current pointer) | Deployments that accept Vault KV ACLs as the sole confidentiality boundary | | Vault Transit | `VaultTransit` | Key-encryption keys never leave Vault; only Transit ciphertext is visible outside | Vault Transit engine (cryptographic isolation) | Delegated to Vault storage | Via Vault Transit key versioning | Deployments that need key material to be unreadable through storage APIs | | AWS KMS | `AWS` (alias `AwsKms`) | Key material never leaves AWS KMS; RustFS mirrors no key state | AWS KMS (cryptographic isolation) + IAM | Delegated to AWS | On-demand `RotateKeyOnDemand`; prior backing keys stay usable for decryption | Deployments already rooted in AWS IAM that want AWS as the cryptographic root — read [AWS KMS: deviations from the shared backend contract](#aws-kms-deviations-from-the-shared-backend-contract) first | @@ -274,7 +274,7 @@ The facts today: - The in-code documentation labels the backend "for development and testing only", and configuration validation enforces stricter rules outside explicit development mode: a master key is required and `key_dir` must not live under the process temp directory. - Production multi-node deployments should use the Vault Transit backend. -The backend's final support level is positioning under review (internal tracking); this section describes what the implementation guarantees, not a commitment to a support tier. +The backend's positioning is settled (owner decision, 2026-08): `Local` is a development, testing and demo backend and is not supported for production. The runtime now states this itself — activating a backend whose capabilities report `production_supported: false` logs a `kms_backend_positioning` warning on every start, restart and reconfigure, and the `kms/status` capability matrix carries the same flag for consoles and tooling. Existing deployments are not blocked: the positioning is a warning, not a gate. This section describes what the implementation guarantees for those who accept that positioning. ### Deployment support matrix diff --git a/rustfs/src/config/cli.rs b/rustfs/src/config/cli.rs index 91bb098b4..d6547218b 100644 --- a/rustfs/src/config/cli.rs +++ b/rustfs/src/config/cli.rs @@ -397,7 +397,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, static, aws + /// KMS backend type: local (development/testing only), vault or vault-kv2 (plain Vault KV v2 storage), vault-transit, static (development/testing only), aws #[arg(long, default_value_t = rustfs_config::DEFAULT_KMS_BACKEND.to_string(), env = "RUSTFS_KMS_BACKEND")] pub kms_backend: String,