mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 18:46:17 +00:00
feat(kms): add AppRole configuration surface for Vault auth
Extend VaultAuthMethod::AppRole with secret_id_file (re-read on every login so external rotation is picked up), a configurable auth mount (default "approle"), and an optional fail-closed safety window. All new fields are serde(default) so previously persisted configurations keep deserializing, and the strict admin-configure deserializer accepts them as optional. Environment selection: setting RUSTFS_KMS_VAULT_APPROLE_ROLE_ID switches both Vault backends to AppRole; the secret_id comes from RUSTFS_KMS_VAULT_APPROLE_SECRET_ID_FILE (path stored, file wins) or RUSTFS_KMS_VAULT_APPROLE_SECRET_ID, following the static secret-key file precedent. validate() rejects AppRole configs without a role_id, without any secret_id source, or with an empty mount. Also append the CredentialsUnavailable error variant used by the fail-closed credential gate.
This commit is contained in:
@@ -235,15 +235,39 @@ pub struct StartKmsRequest {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
enum StrictVaultAuthMethod {
|
enum StrictVaultAuthMethod {
|
||||||
Token { token: String },
|
Token {
|
||||||
AppRole { role_id: String, secret_id: String },
|
token: String,
|
||||||
|
},
|
||||||
|
AppRole {
|
||||||
|
role_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
secret_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
secret_id_file: Option<std::path::PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
mount: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
refresh_safety_window_secs: Option<u64>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StrictVaultAuthMethod> for VaultAuthMethod {
|
impl From<StrictVaultAuthMethod> for VaultAuthMethod {
|
||||||
fn from(value: StrictVaultAuthMethod) -> Self {
|
fn from(value: StrictVaultAuthMethod) -> Self {
|
||||||
match value {
|
match value {
|
||||||
StrictVaultAuthMethod::Token { token } => Self::Token { token },
|
StrictVaultAuthMethod::Token { token } => Self::Token { token },
|
||||||
StrictVaultAuthMethod::AppRole { role_id, secret_id } => Self::AppRole { role_id, secret_id },
|
StrictVaultAuthMethod::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
refresh_safety_window_secs,
|
||||||
|
} => Self::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount: mount.unwrap_or_else(|| crate::config::DEFAULT_VAULT_APPROLE_MOUNT.to_string()),
|
||||||
|
refresh_safety_window_secs,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,10 +896,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest {
|
let approle = ConfigureKmsRequest::VaultKv2(ConfigureVaultKmsRequest {
|
||||||
address: "https://vault.example.com:8200".to_string(),
|
address: "https://vault.example.com:8200".to_string(),
|
||||||
auth_method: VaultAuthMethod::AppRole {
|
auth_method: VaultAuthMethod::approle("configure-role-id".to_string(), "configure-approle-secret-id".to_string()),
|
||||||
role_id: "configure-role-id".to_string(),
|
|
||||||
secret_id: "configure-approle-secret-id".to_string(),
|
|
||||||
},
|
|
||||||
namespace: None,
|
namespace: None,
|
||||||
mount_path: Some("transit".to_string()),
|
mount_path: Some("transit".to_string()),
|
||||||
kv_mount: Some("secret".to_string()),
|
kv_mount: Some("secret".to_string()),
|
||||||
|
|||||||
+247
-8
@@ -29,8 +29,13 @@ pub const ENV_KMS_VAULT_TRANSIT_METADATA_KV_MOUNT: &str = "RUSTFS_KMS_VAULT_TRAN
|
|||||||
pub const ENV_KMS_VAULT_TRANSIT_METADATA_PREFIX: &str = "RUSTFS_KMS_VAULT_TRANSIT_METADATA_PREFIX";
|
pub const ENV_KMS_VAULT_TRANSIT_METADATA_PREFIX: &str = "RUSTFS_KMS_VAULT_TRANSIT_METADATA_PREFIX";
|
||||||
pub const ENV_KMS_STATIC_SECRET_KEY: &str = "RUSTFS_KMS_STATIC_SECRET_KEY";
|
pub const ENV_KMS_STATIC_SECRET_KEY: &str = "RUSTFS_KMS_STATIC_SECRET_KEY";
|
||||||
pub const ENV_KMS_STATIC_SECRET_KEY_FILE: &str = "RUSTFS_KMS_STATIC_SECRET_KEY_FILE";
|
pub const ENV_KMS_STATIC_SECRET_KEY_FILE: &str = "RUSTFS_KMS_STATIC_SECRET_KEY_FILE";
|
||||||
|
pub const ENV_KMS_VAULT_APPROLE_ROLE_ID: &str = "RUSTFS_KMS_VAULT_APPROLE_ROLE_ID";
|
||||||
|
pub const ENV_KMS_VAULT_APPROLE_SECRET_ID: &str = "RUSTFS_KMS_VAULT_APPROLE_SECRET_ID";
|
||||||
|
pub const ENV_KMS_VAULT_APPROLE_SECRET_ID_FILE: &str = "RUSTFS_KMS_VAULT_APPROLE_SECRET_ID_FILE";
|
||||||
|
pub const ENV_KMS_VAULT_APPROLE_MOUNT: &str = "RUSTFS_KMS_VAULT_APPROLE_MOUNT";
|
||||||
pub const DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT: &str = "secret";
|
pub const DEFAULT_VAULT_TRANSIT_METADATA_KV_MOUNT: &str = "secret";
|
||||||
pub const DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX: &str = "rustfs/kms/transit-metadata";
|
pub const DEFAULT_VAULT_TRANSIT_METADATA_KEY_PREFIX: &str = "rustfs/kms/transit-metadata";
|
||||||
|
pub const DEFAULT_VAULT_APPROLE_MOUNT: &str = "approle";
|
||||||
|
|
||||||
/// Upper bound applied to `KmsConfig::timeout` when deriving backend behavior.
|
/// Upper bound applied to `KmsConfig::timeout` when deriving backend behavior.
|
||||||
///
|
///
|
||||||
@@ -53,6 +58,10 @@ fn default_vault_kv2_mount_path() -> String {
|
|||||||
"transit".to_string()
|
"transit".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_vault_approle_mount() -> String {
|
||||||
|
DEFAULT_VAULT_APPROLE_MOUNT.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
|
pub const KMS_CONFIG_REDACTION_RULES: &[RedactionRule] = &[
|
||||||
RedactionRule::new("kms.local.master_key", RedactionLevel::Secret, "local backend key encryption material"),
|
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.token", RedactionLevel::Secret, "vault authentication token"),
|
||||||
@@ -388,18 +397,59 @@ impl Default for VaultTransitConfig {
|
|||||||
pub enum VaultAuthMethod {
|
pub enum VaultAuthMethod {
|
||||||
/// Token authentication
|
/// Token authentication
|
||||||
Token { token: String },
|
Token { token: String },
|
||||||
/// AppRole authentication
|
/// AppRole authentication: login with `role_id` + `secret_id` for a
|
||||||
AppRole { role_id: String, secret_id: String },
|
/// lease-bound token that is renewed in the background.
|
||||||
|
AppRole {
|
||||||
|
role_id: String,
|
||||||
|
/// Inline secret_id; used only when `secret_id_file` is unset.
|
||||||
|
secret_id: String,
|
||||||
|
/// Path to a file holding the secret_id. Re-read on every login so an
|
||||||
|
/// externally rotated secret_id is picked up; takes precedence over the
|
||||||
|
/// inline value.
|
||||||
|
#[serde(default)]
|
||||||
|
secret_id_file: Option<PathBuf>,
|
||||||
|
/// AppRole auth engine mount path.
|
||||||
|
#[serde(default = "default_vault_approle_mount")]
|
||||||
|
mount: String,
|
||||||
|
/// Fail-closed margin in seconds: once the current token is within this
|
||||||
|
/// window of expiry without a successful refresh, requests are refused
|
||||||
|
/// instead of sent with a token that may lapse mid-flight. Defaults to
|
||||||
|
/// the per-attempt timeout.
|
||||||
|
#[serde(default)]
|
||||||
|
refresh_safety_window_secs: Option<u64>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VaultAuthMethod {
|
||||||
|
/// AppRole authentication with the default mount and no secret-id file.
|
||||||
|
pub fn approle(role_id: String, secret_id: String) -> Self {
|
||||||
|
Self::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file: None,
|
||||||
|
mount: default_vault_approle_mount(),
|
||||||
|
refresh_safety_window_secs: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for VaultAuthMethod {
|
impl fmt::Debug for VaultAuthMethod {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Token { token } => f.debug_struct("Token").field("token", &redacted_secret(token)).finish(),
|
Self::Token { token } => f.debug_struct("Token").field("token", &redacted_secret(token)).finish(),
|
||||||
Self::AppRole { role_id, secret_id } => f
|
Self::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
refresh_safety_window_secs,
|
||||||
|
} => f
|
||||||
.debug_struct("AppRole")
|
.debug_struct("AppRole")
|
||||||
.field("role_id", role_id)
|
.field("role_id", role_id)
|
||||||
.field("secret_id", &redacted_secret(secret_id))
|
.field("secret_id", &redacted_secret(secret_id))
|
||||||
|
.field("secret_id_file", secret_id_file)
|
||||||
|
.field("mount", mount)
|
||||||
|
.field("refresh_safety_window_secs", refresh_safety_window_secs)
|
||||||
.finish(),
|
.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,7 +529,7 @@ impl KmsConfig {
|
|||||||
backend: KmsBackend::VaultKv2,
|
backend: KmsBackend::VaultKv2,
|
||||||
backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig {
|
backend_config: BackendConfig::VaultKv2(Box::new(VaultConfig {
|
||||||
address: address.to_string(),
|
address: address.to_string(),
|
||||||
auth_method: VaultAuthMethod::AppRole { role_id, secret_id },
|
auth_method: VaultAuthMethod::approle(role_id, secret_id),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -637,6 +687,8 @@ impl KmsConfig {
|
|||||||
return Err(KmsError::configuration_error("Vault KV2 address must use http or https scheme"));
|
return Err(KmsError::configuration_error("Vault KV2 address must use http or https scheme"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_vault_auth_method("Vault KV2", &config.auth_method)?;
|
||||||
|
|
||||||
if !self.allow_insecure_dev_defaults {
|
if !self.allow_insecure_dev_defaults {
|
||||||
validate_vault_development_defaults("Vault KV2", &config.address, &config.auth_method, config.tls.as_ref())?;
|
validate_vault_development_defaults("Vault KV2", &config.address, &config.auth_method, config.tls.as_ref())?;
|
||||||
}
|
}
|
||||||
@@ -660,6 +712,8 @@ impl KmsConfig {
|
|||||||
return Err(KmsError::configuration_error("Vault Transit address must use http or https scheme"));
|
return Err(KmsError::configuration_error("Vault Transit address must use http or https scheme"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_vault_auth_method("Vault Transit", &config.auth_method)?;
|
||||||
|
|
||||||
if !self.allow_insecure_dev_defaults {
|
if !self.allow_insecure_dev_defaults {
|
||||||
validate_vault_development_defaults(
|
validate_vault_development_defaults(
|
||||||
"Vault Transit",
|
"Vault Transit",
|
||||||
@@ -764,7 +818,7 @@ impl KmsConfig {
|
|||||||
}
|
}
|
||||||
KmsBackend::VaultKv2 => {
|
KmsBackend::VaultKv2 => {
|
||||||
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
|
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
|
||||||
let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token");
|
let auth_method = vault_auth_method_from_env()?;
|
||||||
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
|
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
|
||||||
|
|
||||||
let mount_path = match get_env_opt_str("RUSTFS_KMS_VAULT_MOUNT_PATH") {
|
let mount_path = match get_env_opt_str("RUSTFS_KMS_VAULT_MOUNT_PATH") {
|
||||||
@@ -779,7 +833,7 @@ impl KmsConfig {
|
|||||||
|
|
||||||
config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig {
|
config.backend_config = BackendConfig::VaultKv2(Box::new(VaultConfig {
|
||||||
address,
|
address,
|
||||||
auth_method: VaultAuthMethod::Token { token },
|
auth_method,
|
||||||
namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"),
|
namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"),
|
||||||
mount_path,
|
mount_path,
|
||||||
kv_mount: get_env_str("RUSTFS_KMS_VAULT_KV_MOUNT", "secret"),
|
kv_mount: get_env_str("RUSTFS_KMS_VAULT_KV_MOUNT", "secret"),
|
||||||
@@ -789,12 +843,12 @@ impl KmsConfig {
|
|||||||
}
|
}
|
||||||
KmsBackend::VaultTransit => {
|
KmsBackend::VaultTransit => {
|
||||||
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
|
let address = get_env_str("RUSTFS_KMS_VAULT_ADDRESS", "http://localhost:8200");
|
||||||
let token = get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token");
|
let auth_method = vault_auth_method_from_env()?;
|
||||||
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
|
let skip_tls_verify = get_env_bool(ENV_KMS_VAULT_SKIP_TLS_VERIFY, false);
|
||||||
|
|
||||||
config.backend_config = BackendConfig::VaultTransit(Box::new(VaultTransitConfig {
|
config.backend_config = BackendConfig::VaultTransit(Box::new(VaultTransitConfig {
|
||||||
address,
|
address,
|
||||||
auth_method: VaultAuthMethod::Token { token },
|
auth_method,
|
||||||
namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"),
|
namespace: get_env_opt_str("RUSTFS_KMS_VAULT_NAMESPACE"),
|
||||||
mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"),
|
mount_path: get_env_str("RUSTFS_KMS_VAULT_MOUNT_PATH", "transit"),
|
||||||
metadata_kv_mount: get_env_str(
|
metadata_kv_mount: get_env_str(
|
||||||
@@ -873,6 +927,63 @@ fn is_under_temp_dir(path: &Path) -> bool {
|
|||||||
path.starts_with(std::env::temp_dir())
|
path.starts_with(std::env::temp_dir())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the Vault auth method from environment variables.
|
||||||
|
///
|
||||||
|
/// Setting `RUSTFS_KMS_VAULT_APPROLE_ROLE_ID` selects AppRole authentication;
|
||||||
|
/// the secret_id then comes from `RUSTFS_KMS_VAULT_APPROLE_SECRET_ID_FILE`
|
||||||
|
/// (re-read on every login, mirroring the `RUSTFS_KMS_STATIC_SECRET_KEY_FILE`
|
||||||
|
/// precedent) or inline from `RUSTFS_KMS_VAULT_APPROLE_SECRET_ID`, with the
|
||||||
|
/// file taking precedence. Without a role id the legacy token flow applies.
|
||||||
|
fn vault_auth_method_from_env() -> Result<VaultAuthMethod> {
|
||||||
|
let Some(role_id) = get_env_opt_str(ENV_KMS_VAULT_APPROLE_ROLE_ID) else {
|
||||||
|
return Ok(VaultAuthMethod::Token {
|
||||||
|
token: get_env_str("RUSTFS_KMS_VAULT_TOKEN", "dev-token"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_id_file = get_env_opt_str(ENV_KMS_VAULT_APPROLE_SECRET_ID_FILE).map(PathBuf::from);
|
||||||
|
let secret_id = get_env_opt_str(ENV_KMS_VAULT_APPROLE_SECRET_ID).unwrap_or_default();
|
||||||
|
if secret_id.is_empty() && secret_id_file.is_none() {
|
||||||
|
return Err(KmsError::configuration_error(format!(
|
||||||
|
"Vault AppRole requires {ENV_KMS_VAULT_APPROLE_SECRET_ID} or {ENV_KMS_VAULT_APPROLE_SECRET_ID_FILE} to be set"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VaultAuthMethod::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount: get_env_str(ENV_KMS_VAULT_APPROLE_MOUNT, DEFAULT_VAULT_APPROLE_MOUNT),
|
||||||
|
refresh_safety_window_secs: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_vault_auth_method(backend_name: &str, auth_method: &VaultAuthMethod) -> Result<()> {
|
||||||
|
match auth_method {
|
||||||
|
VaultAuthMethod::Token { .. } => Ok(()),
|
||||||
|
VaultAuthMethod::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if role_id.is_empty() {
|
||||||
|
return Err(KmsError::configuration_error(format!("{backend_name} AppRole role_id cannot be empty")));
|
||||||
|
}
|
||||||
|
if secret_id.is_empty() && secret_id_file.is_none() {
|
||||||
|
return Err(KmsError::configuration_error(format!(
|
||||||
|
"{backend_name} AppRole requires a secret_id or a secret_id_file"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if mount.is_empty() {
|
||||||
|
return Err(KmsError::configuration_error(format!("{backend_name} AppRole mount cannot be empty")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_vault_development_defaults(
|
fn validate_vault_development_defaults(
|
||||||
backend_name: &str,
|
backend_name: &str,
|
||||||
address: &str,
|
address: &str,
|
||||||
@@ -1297,6 +1408,134 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_env_selects_approle_when_role_id_is_set() {
|
||||||
|
with_vars(
|
||||||
|
vec![
|
||||||
|
("RUSTFS_KMS_BACKEND", Some("vault")),
|
||||||
|
("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_ROLE_ID, Some("env-role-id")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_SECRET_ID, Some("env-approle-secret-id")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_MOUNT, Some("approle-alt")),
|
||||||
|
// A stale token env var must not override the AppRole selection.
|
||||||
|
("RUSTFS_KMS_VAULT_TOKEN", Some("vault-token")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let config = KmsConfig::from_env().expect("kms config should load from env");
|
||||||
|
let vault = config.vault_config().expect("vault backend config");
|
||||||
|
let VaultAuthMethod::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
refresh_safety_window_secs,
|
||||||
|
} = &vault.auth_method
|
||||||
|
else {
|
||||||
|
panic!("role id in the environment must select AppRole auth, got {:?}", vault.auth_method);
|
||||||
|
};
|
||||||
|
assert_eq!(role_id, "env-role-id");
|
||||||
|
assert_eq!(secret_id, "env-approle-secret-id");
|
||||||
|
assert_eq!(secret_id_file, &None);
|
||||||
|
assert_eq!(mount, "approle-alt");
|
||||||
|
assert_eq!(refresh_safety_window_secs, &None);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_env_approle_secret_id_file_is_stored_as_path() {
|
||||||
|
with_vars(
|
||||||
|
vec![
|
||||||
|
("RUSTFS_KMS_BACKEND", Some("vault-transit")),
|
||||||
|
("RUSTFS_KMS_VAULT_ADDRESS", Some("https://vault.example.com")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_ROLE_ID, Some("env-role-id")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_SECRET_ID_FILE, Some("/etc/rustfs/approle-secret-id")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let config = KmsConfig::from_env().expect("kms config should load from env");
|
||||||
|
let vault = config.vault_transit_config().expect("vault transit backend config");
|
||||||
|
let VaultAuthMethod::AppRole {
|
||||||
|
secret_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
..
|
||||||
|
} = &vault.auth_method
|
||||||
|
else {
|
||||||
|
panic!("role id in the environment must select AppRole auth");
|
||||||
|
};
|
||||||
|
// The path is stored, not read: the secret_id file is re-read on
|
||||||
|
// every login so external rotation is picked up.
|
||||||
|
assert_eq!(secret_id_file.as_deref(), Some(std::path::Path::new("/etc/rustfs/approle-secret-id")));
|
||||||
|
assert!(secret_id.is_empty());
|
||||||
|
assert_eq!(mount, DEFAULT_VAULT_APPROLE_MOUNT);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_env_approle_requires_secret_id_or_file() {
|
||||||
|
with_vars(
|
||||||
|
vec![
|
||||||
|
("RUSTFS_KMS_BACKEND", Some("vault")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_ROLE_ID, Some("env-role-id")),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_SECRET_ID, None::<&str>),
|
||||||
|
(ENV_KMS_VAULT_APPROLE_SECRET_ID_FILE, None::<&str>),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let error = KmsConfig::from_env().expect_err("approle without a secret_id source must be rejected");
|
||||||
|
assert!(error.to_string().contains(ENV_KMS_VAULT_APPROLE_SECRET_ID));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_approle_config_deserializes_legacy_shape_with_defaults() {
|
||||||
|
// Persisted configurations from before the AppRole implementation only
|
||||||
|
// carry role_id and secret_id; the new fields must fill with defaults.
|
||||||
|
let legacy = serde_json::json!({
|
||||||
|
"AppRole": {
|
||||||
|
"role_id": "legacy-role",
|
||||||
|
"secret_id": "legacy-secret-id",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let auth: VaultAuthMethod = serde_json::from_value(legacy).expect("legacy AppRole config must keep deserializing");
|
||||||
|
let VaultAuthMethod::AppRole {
|
||||||
|
role_id,
|
||||||
|
secret_id_file,
|
||||||
|
mount,
|
||||||
|
refresh_safety_window_secs,
|
||||||
|
..
|
||||||
|
} = auth
|
||||||
|
else {
|
||||||
|
panic!("expected AppRole");
|
||||||
|
};
|
||||||
|
assert_eq!(role_id, "legacy-role");
|
||||||
|
assert_eq!(secret_id_file, None);
|
||||||
|
assert_eq!(mount, DEFAULT_VAULT_APPROLE_MOUNT);
|
||||||
|
assert_eq!(refresh_safety_window_secs, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_rejects_incomplete_approle() {
|
||||||
|
let mut config = KmsConfig::vault_approle(
|
||||||
|
Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||||
|
String::new(),
|
||||||
|
"secret-id".to_string(),
|
||||||
|
);
|
||||||
|
let error = config.validate().expect_err("empty role_id must be rejected");
|
||||||
|
assert!(error.to_string().contains("role_id"));
|
||||||
|
|
||||||
|
config = KmsConfig::vault_approle(
|
||||||
|
Url::parse("https://vault.example.com:8200").expect("vault URL"),
|
||||||
|
"role-id".to_string(),
|
||||||
|
String::new(),
|
||||||
|
);
|
||||||
|
let error = config
|
||||||
|
.validate()
|
||||||
|
.expect_err("approle without secret_id or secret_id_file must be rejected");
|
||||||
|
assert!(error.to_string().contains("secret_id"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_env_requires_vault_development_opt_in() {
|
fn test_from_env_requires_vault_development_opt_in() {
|
||||||
with_vars(
|
with_vars(
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ pub enum KmsError {
|
|||||||
/// Backup/restore bundle contract violation; see [`crate::backup::BackupError`]
|
/// Backup/restore bundle contract violation; see [`crate::backup::BackupError`]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Backup(#[from] crate::backup::BackupError),
|
Backup(#[from] crate::backup::BackupError),
|
||||||
|
|
||||||
|
/// Backend credentials expired or could not be refreshed in time; requests
|
||||||
|
/// fail closed instead of being sent with credentials that may lapse mid-flight
|
||||||
|
#[error("KMS credentials unavailable: {message}")]
|
||||||
|
CredentialsUnavailable { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KmsError {
|
impl KmsError {
|
||||||
@@ -269,6 +274,11 @@ impl KmsError {
|
|||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a credentials unavailable error
|
||||||
|
pub fn credentials_unavailable<S: Into<String>>(message: S) -> Self {
|
||||||
|
Self::CredentialsUnavailable { message: message.into() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from standard library errors
|
/// Convert from standard library errors
|
||||||
|
|||||||
Reference in New Issue
Block a user