mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
fix(keystone): fail closed on EC2 auth and apply configured timeout (#4464)
This commit is contained in:
+40
-18
@@ -120,24 +120,14 @@ impl KeystoneAuthProvider {
|
||||
}
|
||||
|
||||
/// Convert EC2 credential to Keystone token
|
||||
async fn ec2_to_keystone_token(&self, ec2_cred: &EC2Credential) -> Result<KeystoneToken> {
|
||||
// In a real implementation, you'd need to:
|
||||
// 1. Use admin credentials to get user/project details
|
||||
// 2. Or maintain a mapping table
|
||||
// For simplicity, construct a minimal token
|
||||
|
||||
Ok(KeystoneToken {
|
||||
token: String::new(),
|
||||
user_id: ec2_cred.user_id.clone(),
|
||||
username: ec2_cred.user_id.clone(), // Use user_id as username
|
||||
project_id: ec2_cred.project_id.clone(),
|
||||
project_name: ec2_cred.project_id.clone(),
|
||||
domain_id: None,
|
||||
domain_name: None,
|
||||
roles: vec!["Member".to_string()], // Default role
|
||||
expires_at: time::OffsetDateTime::now_utc() + time::Duration::hours(24),
|
||||
issued_at: time::OffsetDateTime::now_utc(),
|
||||
})
|
||||
///
|
||||
/// Fails closed. A correct implementation must resolve the full identity
|
||||
/// (user, project, roles, expiry) from Keystone using admin credentials or
|
||||
/// a trusted mapping. It must never fabricate roles or derive identity from
|
||||
/// client-supplied input. Until that is modeled against a live server, this
|
||||
/// path refuses to mint a token.
|
||||
async fn ec2_to_keystone_token(&self, _ec2_cred: &EC2Credential) -> Result<KeystoneToken> {
|
||||
Err(KeystoneError::Ec2AuthUnsupported)
|
||||
}
|
||||
|
||||
/// Convert Keystone token to RustFS credentials
|
||||
@@ -236,6 +226,7 @@ mod tests {
|
||||
None,
|
||||
"Default".to_string(),
|
||||
true,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
|
||||
let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true);
|
||||
@@ -275,6 +266,7 @@ mod tests {
|
||||
None,
|
||||
"Default".to_string(),
|
||||
true,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
|
||||
let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true);
|
||||
@@ -291,4 +283,34 @@ mod tests {
|
||||
cred.groups = Some(vec!["Admin".to_string()]);
|
||||
assert!(provider.is_admin(&cred));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ec2_authentication_fails_closed() {
|
||||
// The EC2/SigV4 path must never fabricate an identity from client input.
|
||||
// It must fail closed rather than produce credentials.
|
||||
let client = KeystoneClient::new(
|
||||
"http://localhost:5000".to_string(),
|
||||
crate::KeystoneVersion::V3,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"Default".to_string(),
|
||||
true,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
|
||||
let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true).without_cache();
|
||||
|
||||
// A `:`-bearing access key previously shaped user_id/project_id. It must
|
||||
// no longer produce any credentials.
|
||||
let result = provider
|
||||
.authenticate_with_ec2("attacker:admin-project", "sig", "string-to-sign")
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "EC2 auth must not return fabricated credentials");
|
||||
assert!(
|
||||
matches!(result, Err(KeystoneError::Ec2AuthUnsupported)),
|
||||
"EC2 auth must fail closed with Ec2AuthUnsupported"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct KeystoneClient {
|
||||
admin_domain: String,
|
||||
#[allow(dead_code)]
|
||||
verify_ssl: bool,
|
||||
/// Request timeout applied to the underlying HTTP client.
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -49,6 +51,7 @@ impl AdminToken {
|
||||
|
||||
impl KeystoneClient {
|
||||
/// Create new Keystone client
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
auth_url: String,
|
||||
version: KeystoneVersion,
|
||||
@@ -57,6 +60,7 @@ impl KeystoneClient {
|
||||
admin_project: Option<String>,
|
||||
admin_domain: String,
|
||||
verify_ssl: bool,
|
||||
timeout: std::time::Duration,
|
||||
) -> Self {
|
||||
if !verify_ssl {
|
||||
warn!(
|
||||
@@ -67,7 +71,7 @@ impl KeystoneClient {
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(!verify_ssl)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -81,9 +85,15 @@ impl KeystoneClient {
|
||||
admin_project,
|
||||
admin_domain,
|
||||
verify_ssl,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Request timeout applied to the underlying HTTP client.
|
||||
pub fn timeout(&self) -> std::time::Duration {
|
||||
self.timeout
|
||||
}
|
||||
|
||||
/// Validate a Keystone token
|
||||
pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> {
|
||||
match self.version {
|
||||
@@ -209,53 +219,22 @@ impl KeystoneClient {
|
||||
}
|
||||
|
||||
/// Validate EC2 credentials
|
||||
///
|
||||
/// This path is intentionally not implemented and fails closed. A correct
|
||||
/// implementation must derive the identity (user, project, roles, expiry)
|
||||
/// from the Keystone `/v3/ec2tokens` response, not from client-supplied
|
||||
/// input. The previous implementation discarded the response body and
|
||||
/// fabricated an identity by splitting the client access key on `:`, which
|
||||
/// let a caller shape their own user_id/project_id. Until the response
|
||||
/// schema is modeled against a live server, refuse to produce credentials.
|
||||
pub async fn validate_ec2_credentials(
|
||||
&self,
|
||||
access_key: &str,
|
||||
signature: &str,
|
||||
string_to_sign: &str,
|
||||
_access_key: &str,
|
||||
_signature: &str,
|
||||
_string_to_sign: &str,
|
||||
) -> Result<EC2Credential> {
|
||||
let url = format!("{}/v3/ec2tokens", self.auth_url);
|
||||
|
||||
debug!("Validating EC2 credentials: access_key={}", access_key);
|
||||
|
||||
let payload = json!({
|
||||
"auth": {
|
||||
"identity": {
|
||||
"methods": ["ec2"],
|
||||
"ec2": {
|
||||
"access": access_key,
|
||||
"signature": signature,
|
||||
"data": string_to_sign
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| KeystoneError::HttpError(e.to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(KeystoneError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let _body: serde_json::Value = response.json().await.map_err(|e| KeystoneError::ParseError(e.to_string()))?;
|
||||
|
||||
// Parse access key to extract user_id and project_id
|
||||
let (user_id, project_id) = EC2Credential::parse_access_key(access_key).unwrap_or_else(|| (access_key.to_string(), None));
|
||||
|
||||
Ok(EC2Credential {
|
||||
access: access_key.to_string(),
|
||||
secret: String::new(), // Secret not returned in validation
|
||||
user_id,
|
||||
project_id,
|
||||
trust_id: None,
|
||||
})
|
||||
warn!("EC2/SigV4 credential authentication is not supported; refusing to fabricate identity from client input");
|
||||
Err(KeystoneError::Ec2AuthUnsupported)
|
||||
}
|
||||
|
||||
/// Get EC2 credentials for a user
|
||||
@@ -436,6 +415,7 @@ mod tests {
|
||||
Some("admin".to_string()),
|
||||
"Default".to_string(),
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
);
|
||||
|
||||
assert_eq!(client.auth_url, "http://keystone:5000");
|
||||
|
||||
@@ -268,6 +268,22 @@ mod tests {
|
||||
assert!(!config.enable_tenant_prefix);
|
||||
assert!(!config.implicit_tenants);
|
||||
assert_eq!(config.timeout_seconds, 99);
|
||||
|
||||
// The configured timeout must actually be forwarded to the
|
||||
// client, not silently dropped in favour of a hardcoded value.
|
||||
assert_eq!(config.get_timeout(), Duration::from_secs(99));
|
||||
|
||||
let client = crate::KeystoneClient::new(
|
||||
config.auth_url.clone(),
|
||||
KeystoneVersion::V3,
|
||||
config.admin_user.clone(),
|
||||
config.admin_password.clone(),
|
||||
config.admin_project.clone(),
|
||||
config.get_admin_domain(),
|
||||
config.verify_ssl,
|
||||
config.get_timeout(),
|
||||
);
|
||||
assert_eq!(client.timeout(), Duration::from_secs(99));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ pub enum KeystoneError {
|
||||
#[error("Unsupported Keystone version")]
|
||||
UnsupportedVersion,
|
||||
|
||||
/// EC2/SigV4 credential authentication is not supported
|
||||
///
|
||||
/// The EC2 path deliberately fails closed rather than fabricating an
|
||||
/// identity from client-supplied input.
|
||||
#[error("EC2 credential authentication is not supported")]
|
||||
Ec2AuthUnsupported,
|
||||
|
||||
/// Project not found
|
||||
#[error("Project not found")]
|
||||
ProjectNotFound,
|
||||
|
||||
@@ -248,6 +248,7 @@ mod tests {
|
||||
None,
|
||||
"Default".to_string(),
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
);
|
||||
KeystoneIdentityMapper::new(Arc::new(client), true)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
//! config.admin_project.clone(),
|
||||
//! config.get_admin_domain(),
|
||||
//! config.verify_ssl,
|
||||
//! config.get_timeout(),
|
||||
//! );
|
||||
//!
|
||||
//! let auth_provider = KeystoneAuthProvider::new(
|
||||
@@ -141,21 +142,6 @@ pub struct EC2Credential {
|
||||
pub trust_id: Option<String>,
|
||||
}
|
||||
|
||||
impl EC2Credential {
|
||||
/// Parse access key to extract user_id and project_id
|
||||
///
|
||||
/// Format: "user_id:project_id" or "user_id"
|
||||
pub fn parse_access_key(access_key: &str) -> Option<(String, Option<String>)> {
|
||||
if access_key.contains(':') {
|
||||
let parts: Vec<&str> = access_key.split(':').collect();
|
||||
if parts.len() == 2 {
|
||||
return Some((parts[0].to_string(), Some(parts[1].to_string())));
|
||||
}
|
||||
}
|
||||
Some((access_key.to_string(), None))
|
||||
}
|
||||
}
|
||||
|
||||
/// Token cache for performance optimization
|
||||
#[derive(Clone)]
|
||||
pub struct TokenCache {
|
||||
|
||||
@@ -242,6 +242,7 @@ mod tests {
|
||||
None,
|
||||
"Default".to_string(),
|
||||
true,
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
let provider = KeystoneAuthProvider::new(client, 100, Duration::from_secs(60), true);
|
||||
let layer = KeystoneAuthLayer::new(Some(Arc::new(provider)));
|
||||
|
||||
@@ -28,6 +28,7 @@ fn create_test_auth_provider() -> Arc<KeystoneAuthProvider> {
|
||||
Some("admin".to_string()),
|
||||
"Default".to_string(),
|
||||
false, // Don't verify SSL for tests
|
||||
std::time::Duration::from_secs(30),
|
||||
);
|
||||
|
||||
Arc::new(KeystoneAuthProvider::new(client, 1000, std::time::Duration::from_secs(300), true))
|
||||
@@ -256,6 +257,7 @@ fn test_auth_provider_configuration() {
|
||||
Some("test-project".to_string()),
|
||||
"TestDomain".to_string(),
|
||||
true,
|
||||
std::time::Duration::from_secs(30),
|
||||
);
|
||||
|
||||
// Test with caching enabled
|
||||
|
||||
@@ -66,6 +66,7 @@ pub async fn init_keystone_auth(config: KeystoneConfig) -> Result<(), Box<dyn st
|
||||
config.admin_project.clone(),
|
||||
config.get_admin_domain(),
|
||||
config.verify_ssl,
|
||||
config.get_timeout(),
|
||||
);
|
||||
|
||||
let auth_provider = KeystoneAuthProvider::new(client.clone(), config.cache_size, config.get_cache_ttl(), config.enable_cache);
|
||||
|
||||
Reference in New Issue
Block a user