From 7b20554056230f49af666fb50a661e9201dd8c78 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Wed, 8 Jul 2026 09:31:25 +0800 Subject: [PATCH] fix(credentials): fail closed when deriving RPC secret from default credentials (#4402) The internode RPC HMAC secret is derived from the S3 credential pair via `derive_rpc_secret` when `RUSTFS_RPC_SECRET` is unset. The derivation uses the secret key as the HMAC key, so when the default secret key (`rustfsadmin`) is in effect the derived RPC secret is a fixed, publicly computable value. Any network peer can then forge valid `x-rustfs-signature` headers and invoke internode RPC routes (e.g. `read_file_stream`), bypassing S3 IAM entirely. `normalize_rpc_secret` already rejected the literal default when it was supplied directly, but `resolve_rpc_secret` still derived a secret from the default credential pair. Make the derivation path fail closed: refuse to derive while the default secret key is in effect, forcing operators to set `RUSTFS_RPC_SECRET` (or a non-default `RUSTFS_SECRET_KEY`). A default access key paired with a non-default secret key remains safe and is still allowed. Addresses GHSA-68cw-96m3-h2cf (incomplete-fix follow-up to CVE-2026-45039). --- crates/credentials/src/credentials.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/credentials/src/credentials.rs b/crates/credentials/src/credentials.rs index e918e9265..dc8c2b503 100644 --- a/crates/credentials/src/credentials.rs +++ b/crates/credentials/src/credentials.rs @@ -259,7 +259,18 @@ fn resolve_rpc_secret(env_secret: Option<&str>, global_access: Option<&str>, glo } match (global_access, global_secret) { - (Some(access_key), Some(secret_key)) => derive_rpc_secret(access_key, secret_key), + (Some(access_key), Some(secret_key)) => { + // Fail closed: never derive the RPC secret while the default secret + // key is in effect. The derivation uses `secret_key` as the HMAC key, + // so a public default secret yields a publicly computable RPC secret + // that any network peer can use to forge internode RPC signatures. + // Operators running with default credentials must configure + // RUSTFS_RPC_SECRET (or set a non-default RUSTFS_SECRET_KEY) instead. + if secret_key.trim() == DEFAULT_SECRET_KEY { + return None; + } + derive_rpc_secret(access_key, secret_key) + } _ => None, } } @@ -575,12 +586,19 @@ mod tests { } #[test] - fn test_resolve_rpc_secret_allows_default_credentials_for_derivation() { + fn test_resolve_rpc_secret_rejects_default_credentials_for_derivation() { assert!(resolve_rpc_secret(None, None, None).is_none()); - let expected = derive_rpc_secret(DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY).expect("secret should derive"); + // Fail closed: the default secret key must not yield a derivable RPC + // secret, otherwise the derived value is publicly computable and any + // network peer can forge internode RPC signatures. + assert!(resolve_rpc_secret(None, Some(DEFAULT_ACCESS_KEY), Some(DEFAULT_SECRET_KEY)).is_none()); + + // A default access key paired with a non-default secret key is still + // safe to derive: the HMAC key (the secret key) is not public. + let expected = derive_rpc_secret(DEFAULT_ACCESS_KEY, "custom-global-secret").expect("secret should derive"); assert_eq!( - resolve_rpc_secret(None, Some(DEFAULT_ACCESS_KEY), Some(DEFAULT_SECRET_KEY)).as_deref(), + resolve_rpc_secret(None, Some(DEFAULT_ACCESS_KEY), Some("custom-global-secret")).as_deref(), Some(expected.as_str()) );