fix(sse): allow PUT/GET without KMS when no SSE or bucket default (#2054)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
唐小鸭
2026-03-03 00:44:23 +08:00
committed by GitHub
parent bf957e3523
commit f17725a2ea
+162 -22
View File
@@ -167,27 +167,32 @@ async fn prepare_sse_configuration(
}));
}
// Get bucket default encryption configuration
// Get bucket default encryption configuration.
// Only buckets with explicit SSE config return Ok; unconfigured buckets return ConfigNotFound.
let bucket_sse_config_result = metadata_sys::get_sse_config(bucket).await;
debug!("bucket_sse_config_result={:?}", bucket_sse_config_result);
if let Ok((bucket_sse_config, _timestamp)) = bucket_sse_config_result {
let effective_sse = server_side_encryption
.clone()
.or_else(|| {
bucket_sse_config.rules.first().and_then(|rule| {
debug!("Processing SSE rule: {:?}", rule);
rule.apply_server_side_encryption_by_default.as_ref().map(|sse| {
debug!("Found SSE default: {:?}", sse);
match sse.sse_algorithm.as_str() {
"AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
"aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS),
_ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), // fallback to AES256
}
})
// Use request SSE, or bucket's apply_server_side_encryption_by_default. Do NOT default to
// AES256 when neither is set (avoids forcing encryption for buckets without default SSE).
let effective_sse_opt = server_side_encryption.clone().or_else(|| {
bucket_sse_config.rules.first().and_then(|rule| {
debug!("Processing SSE rule: {:?}", rule);
rule.apply_server_side_encryption_by_default.as_ref().map(|sse| {
debug!("Found SSE default: {:?}", sse);
match sse.sse_algorithm.as_str() {
"AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256),
"aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS),
_ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), // fallback
}
})
})
.unwrap_or_else(|| ServerSideEncryption::from_static(ServerSideEncryption::AES256));
});
let Some(effective_sse) = effective_sse_opt else {
debug!("no request SSE and no bucket default SSE; skip encryption");
return Ok(None);
};
debug!("effective_sse={:?} (original={:?})", effective_sse, server_side_encryption);
let effective_kms_key_id = ssekms_key_id.or_else(|| {
@@ -1028,9 +1033,11 @@ async fn apply_managed_encryption_material(
}
}
let kms_key_to_use = kms_key_candidate
.clone()
.ok_or_else(|| ApiError::from(StorageError::other("No KMS key available for managed server-side encryption")))?;
let kms_key_to_use = kms_key_candidate.clone().ok_or_else(|| {
ApiError::from(StorageError::other(
"No KMS key available for managed server-side encryption (required for SSE-KMS)",
))
})?;
let provider = get_sse_dek_provider().await?;
@@ -1390,6 +1397,33 @@ impl TestSseDekProvider {
Self { master_key }
}
/// Create a local SSE DEK provider for SSE-S3 when KMS is not configured.
/// Uses RUSTFS_SSE_S3_MASTER_KEY (base64 32-byte) if set; otherwise a built-in default.
/// Allows PUT/GET to work without KMS (backward compatible).
pub fn new_for_local_sse() -> Self {
let master_key = match std::env::var("RUSTFS_SSE_S3_MASTER_KEY") {
Ok(v) if !v.trim().is_empty() => match BASE64_STANDARD.decode(v.trim()) {
Ok(decoded) if decoded.len() == 32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded[..32]);
tracing::info!("Using RUSTFS_SSE_S3_MASTER_KEY for SSE-S3 (KMS not configured)");
arr
}
_ => {
tracing::warn!("RUSTFS_SSE_S3_MASTER_KEY invalid (expected base64 32 bytes); using default for SSE-S3");
[0u8; 32]
}
},
_ => {
tracing::debug!(
"KMS not configured; using built-in default key for SSE-S3 (set RUSTFS_SSE_S3_MASTER_KEY for production)"
);
[0u8; 32]
}
};
Self { master_key }
}
// Simple encryption of DEK
pub(crate) fn encrypt_dek(dek: [u8; 32], cmk_value: [u8; 32]) -> Result<String, ApiError> {
// Use AES-256-GCM to encrypt DEK
@@ -1501,13 +1535,16 @@ pub async fn get_sse_dek_provider() -> Result<Arc<dyn SseDekProvider>, ApiError>
return Ok(provider.clone());
}
// Determine provider based on environment variable
let provider: Arc<dyn SseDekProvider> = if std::env::var("__RUSTFS_SSE_SIMPLE_CMK").is_ok() {
// Determine provider: KMS when available, else test env, else local SSE-S3 fallback (no KMS)
let provider: Arc<dyn SseDekProvider> = if get_global_encryption_service().await.is_some() {
debug!("Using KmsSseDekProvider (KMS configured)");
Arc::new(KmsSseDekProvider::new().await?)
} else if std::env::var("__RUSTFS_SSE_SIMPLE_CMK").is_ok() {
debug!("Using SimpleSseDekProvider (test mode) based on __RUSTFS_SSE_SIMPLE_CMK");
Arc::new(TestSseDekProvider::new())
} else {
debug!("Using KmsSseDekProvider (production mode)");
Arc::new(KmsSseDekProvider::new().await?)
debug!("Using local SSE-S3 provider (KMS not configured)");
Arc::new(TestSseDekProvider::new_for_local_sse())
};
// Store in global cache
@@ -2654,4 +2691,107 @@ mod tests {
let err = validate_ssec_params(params).unwrap_err();
assert_eq!(err.code, S3ErrorCode::InvalidRequest);
}
// ========================================================================
// Unit tests for issue #2041: no mandatory KMS when encryption not used
// ========================================================================
/// When SSE-C params are not present and no managed SSE is requested,
/// encryption should be skipped (Ok(None)). Ensures we do not require KMS
/// when the client sends no encryption headers.
#[tokio::test]
async fn test_sse_encryption_skip_when_no_ssec_and_no_managed_sse_requested() {
let request = EncryptionRequest {
bucket: "test-bucket",
key: "test-key",
server_side_encryption: None,
ssekms_key_id: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 1024,
part_number: None,
part_key: None,
part_nonce: None,
};
let result = sse_encryption(request).await;
match &result {
Ok(None) => {}
Ok(Some(_)) => panic!("expected no encryption material when no SSE params provided"),
Err(e) => {
assert!(
!e.message.contains("No KMS key"),
"must not require KMS when no encryption requested; got: {}",
e.message
);
}
}
}
/// When SSE-C params are partial or invalid, sse_encryption must return an error.
#[tokio::test]
async fn test_sse_encryption_errors_on_invalid_ssec_params() {
let bucket = "test-bucket";
let key = "test-key";
let sse_key = BASE64_STANDARD.encode([42u8; 32]);
let wrong_md5 = BASE64_STANDARD.encode([99u8; 16]);
let request_wrong_md5 = EncryptionRequest {
bucket,
key,
server_side_encryption: None,
ssekms_key_id: None,
sse_customer_algorithm: Some("AES256".to_string()),
sse_customer_key: Some(sse_key.clone()),
sse_customer_key_md5: Some(wrong_md5),
content_size: 1024,
part_number: None,
part_key: None,
part_nonce: None,
};
let err = sse_encryption(request_wrong_md5).await.unwrap_err();
assert_eq!(err.code, S3ErrorCode::InvalidRequest);
let request_unsupported_algorithm = EncryptionRequest {
bucket,
key,
server_side_encryption: None,
ssekms_key_id: None,
sse_customer_algorithm: Some("unsupported-algo".to_string()),
sse_customer_key: Some(sse_key),
sse_customer_key_md5: Some(BASE64_STANDARD.encode(md5::compute([42u8; 32]).0)),
content_size: 1024,
part_number: None,
part_key: None,
part_nonce: None,
};
let err = sse_encryption(request_unsupported_algorithm).await.unwrap_err();
assert!(err.code == S3ErrorCode::InvalidRequest || err.code == S3ErrorCode::InvalidArgument);
}
/// When bucket has no SSE-S3/aws:kms setting and request has no SSE headers,
/// encryption should be skipped (Ok(None)). Ensures no mandatory bucket default SSE.
#[tokio::test]
async fn test_sse_prepare_encryption_skip_when_no_params_and_no_bucket_sse() {
let request = PrepareEncryptionRequest {
bucket: "test-bucket-no-sse-config",
key: "test-key",
server_side_encryption: None,
ssekms_key_id: None,
sse_customer_algorithm: None,
sse_customer_key_md5: None,
};
let result = sse_prepare_encryption(request).await;
match &result {
Ok(None) => {}
Ok(Some(_)) => panic!("expected no encryption when bucket has no SSE config and no request SSE"),
Err(e) => {
assert!(
!e.message.contains("No KMS key"),
"must not require KMS when no bucket SSE and no request SSE; got: {}",
e.message
);
}
}
}
}