feat: add KMS config redaction safeguards (#3303)

This commit is contained in:
安正超
2026-06-09 22:08:58 +08:00
committed by GitHub
parent 7ec16e197c
commit 03eb10b07f
5 changed files with 329 additions and 35 deletions
Generated
+1
View File
@@ -9564,6 +9564,7 @@ dependencies = [
"moka", "moka",
"rand 0.10.1", "rand 0.10.1",
"reqwest", "reqwest",
"rustfs-security-governance",
"rustfs-utils", "rustfs-utils",
"serde", "serde",
"serde_json", "serde_json",
+1
View File
@@ -57,6 +57,7 @@ moka = { workspace = true, features = ["future"] }
md5 = { workspace = true } md5 = { workspace = true }
arc-swap = { workspace = true } arc-swap = { workspace = true }
rustfs-utils = { workspace = true } rustfs-utils = { workspace = true }
rustfs-security-governance = { workspace = true }
# HTTP client for Vault # HTTP client for Vault
reqwest = { workspace = true } reqwest = { workspace = true }
+119 -1
View File
@@ -16,16 +16,18 @@
use crate::config::{ use crate::config::{
BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig, BackendConfig, CacheConfig, KmsBackend, KmsConfig, LocalConfig, TlsConfig, VaultAuthMethod, VaultConfig, VaultTransitConfig,
redacted_secret_option,
}; };
use crate::service_manager::KmsServiceStatus; use crate::service_manager::KmsServiceStatus;
use crate::types::{KeyMetadata, KeyUsage}; use crate::types::{KeyMetadata, KeyUsage};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
/// Request to configure KMS with Local backend /// Request to configure KMS with Local backend
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct ConfigureLocalKmsRequest { pub struct ConfigureLocalKmsRequest {
/// Directory to store key files /// Directory to store key files
pub key_dir: PathBuf, pub key_dir: PathBuf,
@@ -47,6 +49,23 @@ pub struct ConfigureLocalKmsRequest {
pub cache_ttl_seconds: Option<u64>, pub cache_ttl_seconds: Option<u64>,
} }
impl fmt::Debug for ConfigureLocalKmsRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let master_key = redacted_secret_option(self.master_key.as_deref());
f.debug_struct("ConfigureLocalKmsRequest")
.field("key_dir", &self.key_dir)
.field("master_key", &master_key)
.field("file_permissions", &self.file_permissions)
.field("default_key_id", &self.default_key_id)
.field("timeout_seconds", &self.timeout_seconds)
.field("retry_attempts", &self.retry_attempts)
.field("enable_cache", &self.enable_cache)
.field("max_cached_keys", &self.max_cached_keys)
.field("cache_ttl_seconds", &self.cache_ttl_seconds)
.finish()
}
}
/// Request to configure KMS with Vault KV v2 + Transit backend /// Request to configure KMS with Vault KV v2 + Transit backend
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigureVaultKmsRequest { pub struct ConfigureVaultKmsRequest {
@@ -428,6 +447,7 @@ impl ConfigureKmsRequest {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::config::REDACTED_SECRET;
#[test] #[test]
fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() { fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() {
@@ -536,6 +556,104 @@ mod tests {
other => panic!("expected vault-transit summary, got {other:?}"), other => panic!("expected vault-transit summary, got {other:?}"),
} }
} }
#[test]
fn test_configure_request_debug_redacts_kms_secret_fields() {
let local = ConfigureKmsRequest::Local(ConfigureLocalKmsRequest {
key_dir: PathBuf::from("/tmp/kms"),
master_key: Some("local-configure-master-secret".to_string()),
file_permissions: Some(0o600),
default_key_id: Some("default-key".to_string()),
timeout_seconds: Some(30),
retry_attempts: Some(3),
enable_cache: Some(true),
max_cached_keys: Some(16),
cache_ttl_seconds: Some(60),
});
let vault = ConfigureKmsRequest::VaultTransit(ConfigureVaultTransitKmsRequest {
address: "https://vault.example.com:8200".to_string(),
auth_method: VaultAuthMethod::Token {
token: "configure-vault-token-secret".to_string(),
},
namespace: None,
mount_path: Some("transit".to_string()),
skip_tls_verify: Some(false),
default_key_id: None,
timeout_seconds: None,
retry_attempts: None,
enable_cache: None,
max_cached_keys: None,
cache_ttl_seconds: None,
});
let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest {
address: "https://vault.example.com:8200".to_string(),
auth_method: VaultAuthMethod::AppRole {
role_id: "configure-role-id".to_string(),
secret_id: "configure-approle-secret-id".to_string(),
},
namespace: None,
mount_path: Some("transit".to_string()),
kv_mount: Some("secret".to_string()),
key_path_prefix: Some("rustfs/kms/keys".to_string()),
skip_tls_verify: Some(false),
default_key_id: None,
timeout_seconds: None,
retry_attempts: None,
enable_cache: None,
max_cached_keys: None,
cache_ttl_seconds: None,
});
let rendered = format!("{local:?}\n{vault:?}\n{approle:?}");
assert!(!rendered.contains("local-configure-master-secret"));
assert!(!rendered.contains("configure-vault-token-secret"));
assert!(!rendered.contains("configure-approle-secret-id"));
assert!(rendered.contains("configure-role-id"));
assert!(rendered.contains(REDACTED_SECRET));
}
#[test]
fn test_kms_status_response_omits_secret_values_from_json_and_debug() {
let configs = [
KmsConfig {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from("/tmp/kms"),
master_key: Some("local-summary-master-secret".to_string()),
file_permissions: Some(0o600),
}),
..Default::default()
},
KmsConfig::vault(
url::Url::parse("https://vault.example.com:8200").expect("vault URL"),
"summary-vault-token-secret".to_string(),
),
KmsConfig::vault_approle(
url::Url::parse("https://vault.example.com:8200").expect("vault URL"),
"summary-role-id".to_string(),
"summary-approle-secret-id".to_string(),
),
];
for config in configs {
let summary = KmsConfigSummary::from(&config);
let response = KmsStatusResponse {
status: KmsServiceStatus::Configured,
backend_type: Some(config.backend.clone()),
healthy: None,
config_summary: Some(summary),
};
let json = serde_json::to_string(&response).expect("kms status response should serialize");
let debug = format!("{response:?}");
let rendered = format!("{json}\n{debug}");
assert!(!rendered.contains("local-summary-master-secret"));
assert!(!rendered.contains("summary-vault-token-secret"));
assert!(!rendered.contains("summary-approle-secret-id"));
assert!(rendered.contains("has_master_key") || rendered.contains("has_stored_credentials"));
}
}
} }
// ======================================== // ========================================
+162 -5
View File
@@ -15,12 +15,61 @@
//! KMS configuration management //! KMS configuration management
use crate::error::{KmsError, Result}; use crate::error::{KmsError, Result};
use rustfs_security_governance::{RedactionLevel, RedactionRule};
use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str}; use rustfs_utils::{get_env_bool, get_env_opt_str, get_env_str};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
RedactionRule::new("kms.local.master_key", RedactionLevel::Secret, "local backend key encryption material"),
RedactionRule::new("kms.vault.token", RedactionLevel::Secret, "vault authentication token"),
RedactionRule::new("kms.vault.approle.secret_id", RedactionLevel::Secret, "vault approle secret"),
RedactionRule::new("kms.vault_transit.token", RedactionLevel::Secret, "vault transit authentication token"),
RedactionRule::new(
"kms.vault_transit.approle.secret_id",
RedactionLevel::Secret,
"vault transit approle secret",
),
RedactionRule::new(
"kms.configure.local.master_key",
RedactionLevel::Secret,
"admin configure request local master key",
),
RedactionRule::new(
"kms.configure.vault.token",
RedactionLevel::Secret,
"admin configure request vault authentication token",
),
RedactionRule::new(
"kms.configure.vault.approle.secret_id",
RedactionLevel::Secret,
"admin configure request vault approle secret",
),
RedactionRule::new(
"kms.configure.vault_transit.token",
RedactionLevel::Secret,
"admin configure request vault transit authentication token",
),
RedactionRule::new(
"kms.configure.vault_transit.approle.secret_id",
RedactionLevel::Secret,
"admin configure request vault transit approle secret",
),
];
pub(crate) const REDACTED_SECRET: &str = "***redacted***";
pub(crate) fn redacted_secret(value: &str) -> &'static str {
if value.is_empty() { "" } else { REDACTED_SECRET }
}
pub(crate) fn redacted_secret_option(value: Option<&str>) -> Option<&'static str> {
value.map(redacted_secret)
}
/// KMS backend types /// KMS backend types
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KmsBackend { pub enum KmsBackend {
@@ -69,7 +118,7 @@ impl Default for KmsConfig {
} }
/// Backend-specific configuration /// Backend-specific configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub enum BackendConfig { pub enum BackendConfig {
/// Local backend configuration /// Local backend configuration
Local(LocalConfig), Local(LocalConfig),
@@ -86,8 +135,18 @@ impl Default for BackendConfig {
} }
} }
impl fmt::Debug for BackendConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local(config) => f.debug_tuple("Local").field(config).finish(),
Self::VaultKv2(config) => f.debug_tuple("VaultKv2").field(config).finish(),
Self::VaultTransit(config) => f.debug_tuple("VaultTransit").field(config).finish(),
}
}
}
/// Local KMS backend configuration /// Local KMS backend configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct LocalConfig { pub struct LocalConfig {
/// Directory to store key files /// Directory to store key files
pub key_dir: PathBuf, pub key_dir: PathBuf,
@@ -97,6 +156,17 @@ pub struct LocalConfig {
pub file_permissions: Option<u32>, pub file_permissions: Option<u32>,
} }
impl fmt::Debug for LocalConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let master_key = redacted_secret_option(self.master_key.as_deref());
f.debug_struct("LocalConfig")
.field("key_dir", &self.key_dir)
.field("master_key", &master_key)
.field("file_permissions", &self.file_permissions)
.finish()
}
}
impl Default for LocalConfig { impl Default for LocalConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -108,7 +178,7 @@ impl Default for LocalConfig {
} }
/// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit) /// Vault KV v2 + Transit backend configuration (metadata in KV, key wrapping via Transit)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct VaultConfig { pub struct VaultConfig {
/// Vault server URL /// Vault server URL
pub address: String, pub address: String,
@@ -126,6 +196,20 @@ pub struct VaultConfig {
pub tls: Option<TlsConfig>, pub tls: Option<TlsConfig>,
} }
impl fmt::Debug for VaultConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VaultConfig")
.field("address", &self.address)
.field("auth_method", &self.auth_method)
.field("namespace", &self.namespace)
.field("mount_path", &self.mount_path)
.field("kv_mount", &self.kv_mount)
.field("key_path_prefix", &self.key_path_prefix)
.field("tls", &self.tls)
.finish()
}
}
impl Default for VaultConfig { impl Default for VaultConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -143,7 +227,7 @@ impl Default for VaultConfig {
} }
/// Vault Transit backend configuration /// Vault Transit backend configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct VaultTransitConfig { pub struct VaultTransitConfig {
/// Vault server URL /// Vault server URL
pub address: String, pub address: String,
@@ -157,6 +241,18 @@ pub struct VaultTransitConfig {
pub tls: Option<TlsConfig>, pub tls: Option<TlsConfig>,
} }
impl fmt::Debug for VaultTransitConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VaultTransitConfig")
.field("address", &self.address)
.field("auth_method", &self.auth_method)
.field("namespace", &self.namespace)
.field("mount_path", &self.mount_path)
.field("tls", &self.tls)
.finish()
}
}
impl Default for VaultTransitConfig { impl Default for VaultTransitConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -172,7 +268,7 @@ impl Default for VaultTransitConfig {
} }
/// Vault authentication methods /// Vault authentication methods
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub enum VaultAuthMethod { pub enum VaultAuthMethod {
/// Token authentication /// Token authentication
Token { token: String }, Token { token: String },
@@ -180,6 +276,19 @@ pub enum VaultAuthMethod {
AppRole { role_id: String, secret_id: String }, AppRole { role_id: String, secret_id: String },
} }
impl fmt::Debug for VaultAuthMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Token { token } => f.debug_struct("Token").field("token", &redacted_secret(token)).finish(),
Self::AppRole { role_id, secret_id } => f
.debug_struct("AppRole")
.field("role_id", role_id)
.field("secret_id", &redacted_secret(secret_id))
.finish(),
}
}
}
/// TLS configuration for Vault /// TLS configuration for Vault
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig { pub struct TlsConfig {
@@ -460,6 +569,7 @@ impl KmsConfig {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use rustfs_security_governance::validate_redaction_rules;
use temp_env::with_vars; use temp_env::with_vars;
use tempfile::TempDir; use tempfile::TempDir;
@@ -551,6 +661,53 @@ mod tests {
assert_eq!(serialized, "\"VaultTransit\""); assert_eq!(serialized, "\"VaultTransit\"");
} }
#[test]
fn test_kms_redaction_rules_are_valid() {
assert!(validate_redaction_rules(KMS_CONFIG_REDACTION_RULES).is_ok());
}
#[test]
fn test_kms_config_debug_redacts_secret_fields() {
let local = KmsConfig {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from("/tmp/kms"),
master_key: Some("local-master-secret".to_string()),
file_permissions: Some(0o600),
}),
..Default::default()
};
let vault = KmsConfig::vault(
Url::parse("https://vault.example.com:8200").expect("vault URL"),
"vault-token-secret".to_string(),
);
let approle = KmsConfig::vault_approle(
Url::parse("https://vault.example.com:8200").expect("vault URL"),
"role-id-visible".to_string(),
"approle-secret-id".to_string(),
);
let rendered = format!("{local:?}\n{vault:?}\n{approle:?}");
assert!(!rendered.contains("local-master-secret"));
assert!(!rendered.contains("vault-token-secret"));
assert!(!rendered.contains("approle-secret-id"));
assert!(rendered.contains("role-id-visible"));
assert!(rendered.contains(REDACTED_SECRET));
}
#[test]
fn test_kms_config_serialization_preserves_secret_fields_for_persistence() {
let config = KmsConfig::vault(
Url::parse("https://vault.example.com:8200").expect("vault URL"),
"persisted-token-secret".to_string(),
);
let serialized = serde_json::to_string(&config).expect("kms config should serialize for persistence");
assert!(serialized.contains("persisted-token-secret"));
}
#[test] #[test]
fn test_config_validation() { fn test_config_validation() {
let mut config = KmsConfig::default(); let mut config = KmsConfig::default();
+46 -29
View File
@@ -5,16 +5,20 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
## Current Context ## Current Context
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
- Branch: `overtrue/arch-kms-handler-actions` - Branch: `overtrue/arch-kms-redaction`
- Baseline: `upstream/main` at `4fec606dc4f92b19e085f1609a188e82a72720ff` - Baseline: `upstream/main` at `0cdcd1eb7bfd5fc229eb45f851c624084b072365`
- PR type for this branch: `security-change` - PR type for this branch: `security-change`
- Runtime behavior changes: high-risk KMS admin handlers now authorize through - Runtime behavior changes: KMS secret-bearing `Debug` output and admin status
dedicated `kms:*` actions instead of broad `ServerInfoAdminAction`. summary views no longer expose local master keys, Vault tokens, or AppRole
- Rust code changes: migrate KMS handler action lists and route policy inventory secret IDs. KMS backend behavior, authorization, production defaults, and
to dedicated KMS actions, while keeping temporary legacy create/status admin config persistence are unchanged.
action compatibility with `RUSTFS_COMPAT_TODO(S-012)`. - Rust code changes: add KMS redaction rules, safe `Debug` implementations for
secret-bearing KMS config and configure request types, and focused tests that
prove secrets are absent from debug/admin views while serde persistence keeps
the original values.
- CI/script changes: none - CI/script changes: none
- Docs changes: record S-012 action migration status and temporary compatibility cleanup. - Docs changes: record S-013 redaction status and the no-behavior-drift
migration boundary.
## Phase 0 Tasks ## Phase 0 Tasks
@@ -108,50 +112,63 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
[`compat-cleanup-register.md`](compat-cleanup-register.md). [`compat-cleanup-register.md`](compat-cleanup-register.md).
- Verification: focused handler and route policy tests, migration rules, - Verification: focused handler and route policy tests, migration rules,
formatting, and `make pre-commit`. formatting, and `make pre-commit`.
- [x] `S-013` Apply KMS redaction.
- Acceptance: KMS Debug output and admin status response summaries contain no
Vault token, AppRole secret ID, or local master key values.
- Must preserve: internal KMS config values remain available to runtime code
and persisted config serialization still writes the original secret values.
- Verification: focused KMS redaction/status tests, full KMS tests, migration
guards, Rust quality scan, clippy, and `make pre-commit` passed.
## Next PRs ## Next PRs
1. `contract`: add initial policy inventory tables for redaction, serde, or 1. `security-change`: inventory KMS development defaults before any production
supply-chain governance only after the contract shape remains stable. default hardening.
2. `security-change`: apply KMS response/config redaction after action 2. `security-change`: apply IAM and plugin redaction in a separate S-014 PR.
migration settles.
## Pre-Push Review Log ## Pre-Push Review Log
| Expert | Status | Notes | | Expert | Status | Notes |
|---|---|---| |---|---|---|
| Quality/architecture | pass | Single `security-change` PR; KMS auth action lists are local helper functions, names match `KmsAction`, and no storage/startup/global-state logic is touched. | | Quality/architecture | pass | Single `security-change` PR; redaction rules use the security-governance crate, custom `Debug` stays local to secret-bearing KMS types, and no startup/storage/global-state path is touched. |
| Migration preservation | pass | Legacy create/status admin actions are retained only behind `RUSTFS_COMPAT_TODO(S-012)` and registered for cleanup; broad `ServerInfoAdminAction` is intentionally not retained for high-risk KMS operations. | | Migration preservation | pass | Runtime secret access and persisted config serialization are explicitly preserved by tests; no temporary compatibility path is introduced. |
| Testing/verification | pass | Focused handler/route-policy tests cover dedicated actions and broad-action negative cases; migration rules, formatting, full `make pre-commit`, nextest, and doctests pass. | | Testing/verification | pass | Focused redaction/status tests, full KMS tests, admin KMS handler tests, governance tests, clippy, migration guards, Rust quality scan, nextest, doctests, and `make pre-commit` passed. |
## Verification Notes ## Verification Notes
Passed: Passed:
- Baseline `cargo test -p rustfs admin::handlers::kms --no-fail-fast` - `cargo test -p rustfs-kms redaction -- --nocapture`
- Baseline `cargo test -p rustfs admin::route_policy --no-fail-fast` - `cargo test -p rustfs-kms status_response -- --nocapture`
- `cargo fmt --all --check` - `cargo test -p rustfs-kms --no-fail-fast`
- `cargo test -p rustfs admin::handlers::kms --no-fail-fast` - `cargo test -p rustfs admin::handlers::kms --no-fail-fast`
- `cargo test -p rustfs admin::route_policy --no-fail-fast` - `cargo test -p rustfs-security-governance --no-fail-fast`
- `cargo clippy -p rustfs-kms --all-targets --all-features -- -D warnings`
- Rust code quality scan on changed KMS source files
- `cargo fmt --all --check`
- `./scripts/check_layer_dependencies.sh`
- `./scripts/check_architecture_migration_rules.sh` - `./scripts/check_architecture_migration_rules.sh`
- `./scripts/check_metrics_migration_refs.sh`
- `git diff --check` - `git diff --check`
- `make pre-commit` - `make pre-commit`
Notes: Notes:
- This branch changes only KMS admin authorization action selection and route - This branch changes only KMS redaction for debug/admin view surfaces. It does
policy inventory. It does not change KMS runtime defaults, redaction, startup not change KMS authorization, production defaults, startup order, global
order, global state, storage paths, or crate boundaries. state, storage paths, route registration, or crate boundaries.
- `make pre-commit` passed all checks, including 5682 nextest tests and - Config serialization still preserves secret values for persisted cluster
workspace doctests. config; this is tested explicitly to avoid runtime data loss.
- `make pre-commit` passed all checks, including 5691 nextest tests, 111
skipped tests, and workspace doctests.
## Handoff Notes ## Handoff Notes
- Keep this S-012 branch as a focused `security-change` PR. Do not change KMS - Keep this S-013 branch as a focused `security-change` PR. Do not change KMS
defaults, redaction, admin route registration shape, Config moves, Storage API defaults, admin authorization, admin route registration shape, Config moves,
moves, Runtime moves, or ECStore moves. Storage API moves, Runtime moves, or ECStore moves.
- `rustfs` may depend on `rustfs-security-governance` for contract metadata; - `rustfs` may depend on `rustfs-security-governance` for contract metadata;
the security-governance crate must stay independent from implementation the security-governance crate must stay independent from implementation
crates and runtime state. crates and runtime state.
- Do not add temporary compatibility code without a matching - Do not add temporary compatibility code without a matching
`RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry. `RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry.
- The next KMS security PR should handle redaction or production default - KMS production default hardening remains a separate task group; do not bundle
hardening separately; do not bundle those with this action migration. it with this redaction PR.