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:
Zhengchao An
2026-08-01 22:31:25 +08:00
committed by GitHub
parent 02aa383598
commit f2d09d1426
14 changed files with 1847 additions and 1 deletions
+15
View File
@@ -74,6 +74,7 @@ pub const LOCAL_BUNDLE_MANIFEST_FILE: &str = "manifest.json";
const ARTIFACTS_DIR: &str = "artifacts";
const KEYS_DIR: &str = "artifacts/keys";
const SALT_ARTIFACT_PATH: &str = "artifacts/master-key.salt.enc";
const CONFIG_ARTIFACT_PATH: &str = "artifacts/kms-config.json.enc";
pub(crate) const AEAD_NONCE_LEN: usize = 12;
/// Domain-separation context for the artifact AAD binding.
const BUNDLE_AAD_CONTEXT: &str = "rustfs-kms-local-backup:v1";
@@ -141,6 +142,15 @@ pub struct LocalBackupExportRequest {
pub snapshot_generation: u64,
/// Bundle output directory; must not exist yet or must be empty.
pub destination: PathBuf,
/// Serialized sanitized KMS configuration to seal into the bundle, if the
/// caller produced one.
///
/// The persisted `KmsConfig` carries plaintext credentials (Vault token,
/// AppRole secret id, Local master key), so the sanitized projection is
/// owned by the admin layer and this module only seals the bytes it is
/// handed. The artifact is evidence for an operator decision: restore
/// verifies it opens but never applies a configuration.
pub sanitized_config: Option<Vec<u8>>,
}
impl LocalBackupExportRequest {
@@ -410,6 +420,10 @@ async fn build_and_write_bundle(
let descriptor = encrypt_and_write_artifact(kek, request, ArtifactKind::MasterKeySalt, SALT_ARTIFACT_PATH, salt).await?;
artifacts.push(descriptor);
}
if let Some(config) = &request.sanitized_config {
let descriptor = encrypt_and_write_artifact(kek, request, ArtifactKind::KmsConfig, CONFIG_ARTIFACT_PATH, config).await?;
artifacts.push(descriptor);
}
// Make the artifact directory entries durable before sealing: the sealed
// manifest must never survive a crash that its artifacts did not.
@@ -673,6 +687,7 @@ mod tests {
rustfs_version: "1.0.0-test".to_string(),
snapshot_generation: 7,
destination,
sanitized_config: None,
}
}