mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 00:17:11 +00:00
feat(kms): surface non-production backend positioning at runtime (#6633)
This commit is contained in:
@@ -821,6 +821,7 @@ impl KmsBackend for AwsKmsBackend {
|
|||||||
.with_schedule_deletion(true)
|
.with_schedule_deletion(true)
|
||||||
.with_versioning(true)
|
.with_versioning(true)
|
||||||
.with_physical_delete(false)
|
.with_physical_delete(false)
|
||||||
|
.with_production_supported(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observe, never destroy.
|
/// Observe, never destroy.
|
||||||
|
|||||||
@@ -2279,7 +2279,10 @@ impl KmsBackend for LocalKmsBackend {
|
|||||||
fn capabilities(&self) -> BackendCapabilities {
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
// Rotation stays unadvertised until historical key versions can be
|
// Rotation stays unadvertised until historical key versions can be
|
||||||
// retained (see LocalKmsClient::rotate_key); without version history
|
// 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()
|
BackendCapabilities::minimal()
|
||||||
.with_enable_disable(true)
|
.with_enable_disable(true)
|
||||||
.with_schedule_deletion(true)
|
.with_schedule_deletion(true)
|
||||||
|
|||||||
@@ -615,6 +615,14 @@ pub struct BackendCapabilities {
|
|||||||
pub update_key_metadata: bool,
|
pub update_key_metadata: bool,
|
||||||
/// Re-wrapping an existing data key envelope onto the key's current version
|
/// Re-wrapping an existing data key envelope onto the key's current version
|
||||||
pub rewrap: bool,
|
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 {
|
impl BackendCapabilities {
|
||||||
@@ -633,6 +641,7 @@ impl BackendCapabilities {
|
|||||||
physical_delete: false,
|
physical_delete: false,
|
||||||
update_key_metadata: false,
|
update_key_metadata: false,
|
||||||
rewrap: false,
|
rewrap: false,
|
||||||
|
production_supported: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,6 +704,12 @@ impl BackendCapabilities {
|
|||||||
self.rewrap = rewrap;
|
self.rewrap = rewrap;
|
||||||
self
|
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 {
|
impl Default for BackendCapabilities {
|
||||||
@@ -775,6 +790,30 @@ mod tests {
|
|||||||
assert!(!capabilities.physical_delete);
|
assert!(!capabilities.physical_delete);
|
||||||
assert!(!capabilities.update_key_metadata);
|
assert!(!capabilities.update_key_metadata);
|
||||||
assert!(!capabilities.rewrap);
|
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]
|
#[tokio::test]
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": false,
|
"physical_delete": false,
|
||||||
|
"production_supported": true,
|
||||||
"rewrap": false,
|
"rewrap": false,
|
||||||
"rotate": true,
|
"rotate": true,
|
||||||
"schedule_deletion": true,
|
"schedule_deletion": true,
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": true,
|
"physical_delete": true,
|
||||||
|
"production_supported": false,
|
||||||
"rewrap": false,
|
"rewrap": false,
|
||||||
"rotate": false,
|
"rotate": false,
|
||||||
"schedule_deletion": true,
|
"schedule_deletion": true,
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": false,
|
"physical_delete": false,
|
||||||
|
"production_supported": false,
|
||||||
"rewrap": false,
|
"rewrap": false,
|
||||||
"rotate": false,
|
"rotate": false,
|
||||||
"schedule_deletion": false,
|
"schedule_deletion": false,
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": true,
|
"physical_delete": true,
|
||||||
|
"production_supported": true,
|
||||||
"rewrap": true,
|
"rewrap": true,
|
||||||
"rotate": true,
|
"rotate": true,
|
||||||
"schedule_deletion": true,
|
"schedule_deletion": true,
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
|
|||||||
"encrypt": true,
|
"encrypt": true,
|
||||||
"generate_data_key": true,
|
"generate_data_key": true,
|
||||||
"physical_delete": true,
|
"physical_delete": true,
|
||||||
|
"production_supported": true,
|
||||||
"rewrap": true,
|
"rewrap": true,
|
||||||
"rotate": true,
|
"rotate": true,
|
||||||
"schedule_deletion": true,
|
"schedule_deletion": true,
|
||||||
|
|||||||
@@ -413,7 +413,9 @@ impl KmsBackend for StaticKmsBackend {
|
|||||||
|
|
||||||
fn capabilities(&self) -> BackendCapabilities {
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
// Static KMS is a read-only single-key backend: it only performs
|
// 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()
|
BackendCapabilities::minimal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2257,6 +2257,7 @@ impl KmsBackend for VaultKmsBackend {
|
|||||||
.with_physical_delete(true)
|
.with_physical_delete(true)
|
||||||
.with_update_key_metadata(true)
|
.with_update_key_metadata(true)
|
||||||
.with_rewrap(true)
|
.with_rewrap(true)
|
||||||
|
.with_production_supported(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result<ExpiredKeyRemoval> {
|
async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result<ExpiredKeyRemoval> {
|
||||||
|
|||||||
@@ -1767,6 +1767,7 @@ impl KmsBackend for VaultTransitKmsBackend {
|
|||||||
.with_physical_delete(true)
|
.with_physical_delete(true)
|
||||||
.with_update_key_metadata(true)
|
.with_update_key_metadata(true)
|
||||||
.with_rewrap(true)
|
.with_rewrap(true)
|
||||||
|
.with_production_supported(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result<ExpiredKeyRemoval> {
|
async fn remove_expired_key(&self, key_id: &str, now: &Zoned) -> Result<ExpiredKeyRemoval> {
|
||||||
|
|||||||
@@ -941,8 +941,15 @@ impl KmsConfig {
|
|||||||
&& let Some(ref tls) = config.tls
|
&& let Some(ref tls) = config.tls
|
||||||
&& !tls.skip_verify
|
&& !tls.skip_verify
|
||||||
{
|
{
|
||||||
// In production, we should have proper TLS configuration
|
if tls.ca_cert_path.is_some() || tls.client_cert_path.is_some() || tls.client_key_path.is_some() {
|
||||||
if tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() {
|
// 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");
|
tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -978,10 +985,17 @@ impl KmsConfig {
|
|||||||
if config.address.starts_with("https://")
|
if config.address.starts_with("https://")
|
||||||
&& let Some(ref tls) = config.tls
|
&& let Some(ref tls) = config.tls
|
||||||
&& !tls.skip_verify
|
&& !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) => {
|
BackendConfig::Static(config) => {
|
||||||
|
|||||||
@@ -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
|
// Create KMS manager
|
||||||
//
|
//
|
||||||
// The deletion reference checker is handed to the manager as well as to
|
// The deletion reference checker is handed to the manager as well as to
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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) | Simple deployments with an external secret manager |
|
| 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 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 |
|
| 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 |
|
| 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.
|
- 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.
|
- 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
|
### Deployment support matrix
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ pub struct ServerOpts {
|
|||||||
#[arg(long, default_value_t = false, env = "RUSTFS_KMS_ENABLE")]
|
#[arg(long, default_value_t = false, env = "RUSTFS_KMS_ENABLE")]
|
||||||
pub kms_enable: bool,
|
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")]
|
#[arg(long, default_value_t = rustfs_config::DEFAULT_KMS_BACKEND.to_string(), env = "RUSTFS_KMS_BACKEND")]
|
||||||
pub kms_backend: String,
|
pub kms_backend: String,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user