Harden admin and RPC security checks (#2773)

Signed-off-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
安正超
2026-05-03 19:55:09 +08:00
committed by GitHub
parent eb23710d2e
commit 66c38b629d
10 changed files with 490 additions and 64 deletions
+88 -7
View File
@@ -29,6 +29,12 @@ static GLOBAL_ACTIVE_CRED: OnceLock<Credentials> = OnceLock::new();
/// Global RPC authentication token
pub static GLOBAL_RUSTFS_RPC_SECRET: OnceLock<String> = OnceLock::new();
/// Public error returned when RPC authentication is not safely configured.
pub const RPC_SECRET_REQUIRED_MESSAGE: &str = "RPC authentication secret is not configured";
/// Operator-facing guidance for configuring RPC authentication safely.
pub const RPC_SECRET_REQUIRED_OPERATOR_MESSAGE: &str = "RUSTFS_RPC_SECRET must be set to a non-default value or RUSTFS_SECRET_KEY must be changed from the default for RPC authentication";
/// Error type for credentials operations
#[derive(Debug)]
pub enum CredentialsError {
@@ -216,13 +222,39 @@ pub fn gen_secret_key(length: usize) -> std::io::Result<String> {
/// # Returns
/// * `String` - The RPC authentication token
///
fn resolve_rpc_secret(env_secret: Option<&str>, global_secret: Option<&str>) -> Option<String> {
if let Some(secret) = env_secret.map(str::trim).filter(|secret| !secret.is_empty()) {
return (secret != DEFAULT_SECRET_KEY).then(|| secret.to_string());
}
global_secret
.map(str::trim)
.filter(|secret| !secret.is_empty() && *secret != DEFAULT_SECRET_KEY)
.map(ToOwned::to_owned)
}
pub fn try_get_rpc_token() -> std::io::Result<String> {
if let Some(secret) = GLOBAL_RUSTFS_RPC_SECRET.get() {
return resolve_rpc_secret(None, Some(secret)).ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE));
}
let env_secret = env::var(ENV_RPC_SECRET).ok();
let global_secret = get_global_secret_key_opt();
let secret = resolve_rpc_secret(env_secret.as_deref(), global_secret.as_deref())
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE))?;
match GLOBAL_RUSTFS_RPC_SECRET.set(secret.clone()) {
Ok(()) => Ok(secret),
Err(_) => GLOBAL_RUSTFS_RPC_SECRET
.get()
.and_then(|stored| resolve_rpc_secret(None, Some(stored)))
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE)),
}
}
#[deprecated(note = "use try_get_rpc_token to handle missing RPC secrets explicitly")]
pub fn get_rpc_token() -> String {
GLOBAL_RUSTFS_RPC_SECRET
.get_or_init(|| {
env::var(ENV_RPC_SECRET)
.unwrap_or_else(|_| get_global_secret_key_opt().unwrap_or_else(|| DEFAULT_SECRET_KEY.to_string()))
})
.clone()
try_get_rpc_token().expect(RPC_SECRET_REQUIRED_MESSAGE)
}
/// A wrapper struct for masking sensitive strings in Debug implementations.
@@ -301,7 +333,7 @@ impl fmt::Debug for Credentials {
f.debug_struct("Credentials")
.field("access_key", &self.access_key)
.field("secret_key", &Masked(Some(&self.secret_key)))
.field("session_token", &self.session_token)
.field("session_token", &Masked(Some(&self.session_token)))
.field("expiration", &self.expiration)
.field("status", &self.status)
.field("parent_user", &self.parent_user)
@@ -495,6 +527,37 @@ mod tests {
assert!(!key.contains('='));
}
#[test]
fn test_resolve_rpc_secret_rejects_default_fallback() {
assert!(resolve_rpc_secret(None, None).is_none());
assert!(resolve_rpc_secret(None, Some(DEFAULT_SECRET_KEY)).is_none());
assert!(resolve_rpc_secret(Some(DEFAULT_SECRET_KEY), Some("custom-global-secret")).is_none());
}
#[test]
fn test_rpc_secret_public_error_omits_configuration_details() {
assert!(!RPC_SECRET_REQUIRED_MESSAGE.contains("RUSTFS_"));
assert!(!RPC_SECRET_REQUIRED_MESSAGE.contains(DEFAULT_SECRET_KEY));
assert!(RPC_SECRET_REQUIRED_OPERATOR_MESSAGE.contains("RUSTFS_RPC_SECRET"));
}
#[allow(deprecated)]
#[test]
fn test_get_rpc_token_preserves_string_return_type() {
fn assert_string_return(_: fn() -> String) {}
assert_string_return(get_rpc_token);
}
#[test]
fn test_resolve_rpc_secret_accepts_non_default_secret() {
assert_eq!(resolve_rpc_secret(Some("custom-rpc-secret"), None).as_deref(), Some("custom-rpc-secret"));
assert_eq!(
resolve_rpc_secret(None, Some("custom-global-secret")).as_deref(),
Some("custom-global-secret")
);
}
#[test]
fn test_masked_debug() {
// Test None
@@ -524,6 +587,24 @@ mod tests {
assert_eq!(format!("{:?}", Masked(Some("中文测试"))), "中***试|4");
}
#[test]
fn test_credentials_debug_masks_sensitive_fields() {
let cred = Credentials {
access_key: "debug-access-key".to_string(),
secret_key: "debug-secret-key".to_string(),
session_token: "debug-session-token".to_string(),
parent_user: "parent-user".to_string(),
..Default::default()
};
let output = format!("{cred:?}");
assert!(output.contains("debug-access-key"));
assert!(output.contains("parent-user"));
assert!(!output.contains("debug-secret-key"));
assert!(!output.contains("debug-session-token"));
}
#[test]
fn test_credentials_expiration_serialize_as_rfc3339() {
use time::OffsetDateTime;