mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-03 03:47:42 +00:00
feat(admin): expose KMS backup and restore behind explicit guards (#5579)
* feat(kms): add backup and restore admin API Wires the merged KMS backup contract, Local export and Local restore into the admin API: export a sealed bundle, run a zero-write restore preflight, execute a confirmed restore, roll an interrupted restore back, and report subsystem readiness. - Dedicated kms:Backup / kms:Restore actions, recorded in the admin route matrix. Neither is reachable through any other KMS action. - Restore requires two independent confirmations: an echo of the bundle manifest's backup id, and an explicitly named conflict policy (the default never writes). - The backup KEK comes from the environment and is refused when it reuses a secret of the configured backend, compared both as the literal value and as raw key bytes. - No endpoint accepts a path: bundles are addressed by a validated name under a configured root, and the restore target is always the server's own configured key directory. - Bundles now carry a sanitized configuration artifact built as an allowlist projection, so a future backend credential field cannot leak into a bundle by default. Restore verifies it and never applies it. - Audit entries go through the existing KMS admin wiring and carry identifiers only. * test(kms): pin the backup admin API gates Fixes the test KEK to a real 32-byte value and drives the export refusal from the configured backend rather than from the handle that happens to be available, so a Local handle cannot export on behalf of a backend whose material RustFS does not own.
This commit is contained in:
@@ -288,6 +288,7 @@ atoi = { workspace = true }
|
||||
atomic_enum = { workspace = true }
|
||||
async_zip = { workspace = true, default-features = false, features = ["tokio", "deflate"] }
|
||||
base64 = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
rsa = { workspace = true, features = ["sha2"] }
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! KMS admin handlers for HTTP API
|
||||
|
||||
use super::{kms_dynamic, kms_key_lifecycle, kms_keys, kms_management};
|
||||
use super::{kms_backup, kms_dynamic, kms_key_lifecycle, kms_keys, kms_management};
|
||||
use crate::admin::router::{AdminOperation, S3Router};
|
||||
|
||||
pub fn register_kms_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||
@@ -22,5 +22,6 @@ pub fn register_kms_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<(
|
||||
kms_dynamic::register_kms_dynamic_route(r)?;
|
||||
kms_keys::register_kms_key_route(r)?;
|
||||
kms_key_lifecycle::register_kms_key_lifecycle_route(r)?;
|
||||
kms_backup::register_kms_backup_route(r)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,6 +98,16 @@ pub(super) enum KmsAdminOperation {
|
||||
Start,
|
||||
/// Stop of the KMS service.
|
||||
Stop,
|
||||
/// Export of a backup bundle.
|
||||
Backup,
|
||||
/// Readiness lookup for the backup subsystem.
|
||||
BackupStatus,
|
||||
/// Zero-write restore preflight.
|
||||
RestoreDryRun,
|
||||
/// Execution of a restore.
|
||||
Restore,
|
||||
/// Roll-back of an interrupted restore.
|
||||
RestoreAbort,
|
||||
}
|
||||
|
||||
impl KmsAdminOperation {
|
||||
@@ -109,6 +119,11 @@ impl KmsAdminOperation {
|
||||
Self::Reconfigure => "Reconfigure",
|
||||
Self::Start => "Start",
|
||||
Self::Stop => "Stop",
|
||||
Self::Backup => "Backup",
|
||||
Self::BackupStatus => "BackupStatus",
|
||||
Self::RestoreDryRun => "RestoreDryRun",
|
||||
Self::Restore => "Restore",
|
||||
Self::RestoreAbort => "RestoreAbort",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +136,18 @@ impl KmsAdminOperation {
|
||||
Self::Configure | Self::Reconfigure => EventName::KmsServiceConfigured,
|
||||
Self::Start => EventName::KmsServiceStarted,
|
||||
Self::Stop => EventName::KmsServiceStopped,
|
||||
// A backup reads the material of every key, and a restore
|
||||
// preflight reads a bundle without touching the target: both are
|
||||
// key access. The `kmsOperation` tag distinguishes them, and the
|
||||
// event-name space is a fixed 64-bit mask that is nearly full, so
|
||||
// these reuse the existing access event rather than claiming two
|
||||
// more bits of it.
|
||||
Self::Backup | Self::BackupStatus | Self::RestoreDryRun => EventName::KmsKeyAccessed,
|
||||
// A restore publishes key material into the target directory.
|
||||
Self::Restore => EventName::KmsKeyCreated,
|
||||
// Aborting an interrupted restore removes the material a partial
|
||||
// cutover had already published.
|
||||
Self::RestoreAbort => EventName::KmsKeyDeleted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,6 +572,11 @@ mod tests {
|
||||
KmsAdminOperation::Reconfigure,
|
||||
KmsAdminOperation::Start,
|
||||
KmsAdminOperation::Stop,
|
||||
KmsAdminOperation::Backup,
|
||||
KmsAdminOperation::BackupStatus,
|
||||
KmsAdminOperation::RestoreDryRun,
|
||||
KmsAdminOperation::Restore,
|
||||
KmsAdminOperation::RestoreAbort,
|
||||
] {
|
||||
assert!(
|
||||
operation.event().is_kms(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ pub mod inspect_archive;
|
||||
pub mod is_admin;
|
||||
pub mod kms;
|
||||
pub mod kms_audit;
|
||||
pub mod kms_backup;
|
||||
pub mod kms_dynamic;
|
||||
pub mod kms_key_lifecycle;
|
||||
pub mod kms_keys;
|
||||
|
||||
@@ -54,6 +54,7 @@ const HEALTH_INFO: AdminActionRef = AdminActionRef::new("HealthInfoAdminAction")
|
||||
const IMPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ImportBucketMetadataAction");
|
||||
const IMPORT_IAM: AdminActionRef = AdminActionRef::new("ImportIAMAction");
|
||||
const INSPECT_DATA: AdminActionRef = AdminActionRef::new("InspectDataAction");
|
||||
const KMS_BACKUP: AdminActionRef = AdminActionRef::new("kms:Backup");
|
||||
const KMS_CLEAR_CACHE: AdminActionRef = AdminActionRef::new("kms:ClearCache");
|
||||
const KMS_CONFIGURE: AdminActionRef = AdminActionRef::new("kms:Configure");
|
||||
const KMS_DELETE_KEY: AdminActionRef = AdminActionRef::new("kms:DeleteKey");
|
||||
@@ -62,6 +63,7 @@ const KMS_DISABLE_KEY: AdminActionRef = AdminActionRef::new("kms:DisableKey");
|
||||
const KMS_ENABLE_KEY: AdminActionRef = AdminActionRef::new("kms:EnableKey");
|
||||
const KMS_GENERATE_DATA_KEY: AdminActionRef = AdminActionRef::new("kms:GenerateDataKey");
|
||||
const KMS_LIST_KEYS: AdminActionRef = AdminActionRef::new("kms:ListKeys");
|
||||
const KMS_RESTORE: AdminActionRef = AdminActionRef::new("kms:Restore");
|
||||
const KMS_ROTATE_KEY: AdminActionRef = AdminActionRef::new("kms:RotateKey");
|
||||
const KMS_SERVICE_CONTROL: AdminActionRef = AdminActionRef::new("kms:ServiceControl");
|
||||
const LIST_GROUPS: AdminActionRef = AdminActionRef::new("ListGroupsAdminAction");
|
||||
@@ -788,6 +790,18 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
||||
RouteRiskLevel::High,
|
||||
),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/keys/rotate", KMS_ROTATE_KEY, RouteRiskLevel::High),
|
||||
// Backup and restore act on the material of every key at once, so they
|
||||
// carry their own actions rather than reusing any per-key one.
|
||||
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/backup", KMS_BACKUP, RouteRiskLevel::Sensitive),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/backup", KMS_BACKUP, RouteRiskLevel::High),
|
||||
admin(
|
||||
HttpMethod::Post,
|
||||
"/rustfs/admin/v3/kms/restore/dry-run",
|
||||
KMS_RESTORE,
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/restore", KMS_RESTORE, RouteRiskLevel::High),
|
||||
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/restore/abort", KMS_RESTORE, RouteRiskLevel::High),
|
||||
public(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/oidc/providers",
|
||||
@@ -1826,6 +1840,41 @@ mod tests {
|
||||
assert_action(HttpMethod::Delete, "/rustfs/admin/v3/kms/keys/delete", KMS_DELETE_KEY);
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/kms/keys/cancel-deletion", KMS_DELETE_KEY);
|
||||
assert_action(HttpMethod::Get, "/rustfs/admin/v3/kms/keys/{key_id}", KMS_DESCRIBE_KEY);
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/kms/backup", KMS_BACKUP);
|
||||
assert_action(HttpMethod::Get, "/rustfs/admin/v3/kms/backup", KMS_BACKUP);
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/kms/restore", KMS_RESTORE);
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/kms/restore/dry-run", KMS_RESTORE);
|
||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/kms/restore/abort", KMS_RESTORE);
|
||||
}
|
||||
|
||||
/// Backup and restore expose the whole key inventory at once, so no other
|
||||
/// KMS action may reach them: holding `kms:Configure` or a per-key action
|
||||
/// must not be enough.
|
||||
#[test]
|
||||
fn route_policy_isolates_backup_and_restore_from_other_kms_actions() {
|
||||
for (method, path) in [
|
||||
(HttpMethod::Get, "/rustfs/admin/v3/kms/backup"),
|
||||
(HttpMethod::Post, "/rustfs/admin/v3/kms/backup"),
|
||||
(HttpMethod::Post, "/rustfs/admin/v3/kms/restore"),
|
||||
(HttpMethod::Post, "/rustfs/admin/v3/kms/restore/dry-run"),
|
||||
(HttpMethod::Post, "/rustfs/admin/v3/kms/restore/abort"),
|
||||
] {
|
||||
for action in [
|
||||
SERVER_INFO,
|
||||
KMS_CONFIGURE,
|
||||
KMS_DESCRIBE_KEY,
|
||||
KMS_LIST_KEYS,
|
||||
KMS_SERVICE_CONTROL,
|
||||
KMS_DELETE_KEY,
|
||||
] {
|
||||
assert_not_action(method, path, action);
|
||||
}
|
||||
}
|
||||
|
||||
// Backup and restore are separate privileges: neither implies the
|
||||
// other.
|
||||
assert_not_action(HttpMethod::Post, "/rustfs/admin/v3/kms/backup", KMS_RESTORE);
|
||||
assert_not_action(HttpMethod::Post, "/rustfs/admin/v3/kms/restore", KMS_BACKUP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -343,6 +343,11 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
|
||||
admin_route(Method::POST, "/v3/kms/keys/enable"),
|
||||
admin_route(Method::POST, "/v3/kms/keys/disable"),
|
||||
admin_route(Method::POST, "/v3/kms/keys/rotate"),
|
||||
admin_route(Method::GET, "/v3/kms/backup"),
|
||||
admin_route(Method::POST, "/v3/kms/backup"),
|
||||
admin_route(Method::POST, "/v3/kms/restore/dry-run"),
|
||||
admin_route(Method::POST, "/v3/kms/restore"),
|
||||
admin_route(Method::POST, "/v3/kms/restore/abort"),
|
||||
admin_route(Method::GET, "/v3/oidc/providers"),
|
||||
admin_route_sample(Method::GET, "/v3/oidc/authorize/{provider_id}", "/v3/oidc/authorize/default"),
|
||||
admin_route_sample(Method::GET, "/v3/oidc/callback/{provider_id}", "/v3/oidc/callback/default"),
|
||||
|
||||
Reference in New Issue
Block a user