mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 16:16:55 +00:00
fix(replication): rebuild SSE metadata boundary for encrypted objects (#5872)
Groundwork for encrypted-object replication (backlog#1783, PR-A of 3): - classify_replication_source_encryption: accept the AES256 marker that every stored SSE-C object carries; the SseC arm was unreachable. - Fail closed on sealed material without an SSE marker (MinIO-written objects) instead of replicating ciphertext as plaintext. - Replace the dead VALID_SSE_REPLICATION_HEADERS table with a transport map keyed by the metadata keys the SSE writer actually persists, shared via the new rustfs_utils::http::object_encryption_keys module. - Structurally strip all encryption metadata from outbound replication (x-rustfs-encryption-* envelopes previously passed the filters). - Skip decrypt_checksums for encrypted objects at the boundary so its is_multipart=false (a response-path contract) cannot misroute encrypted multipart objects once managed replication opens. - Redact X-Rustfs-Replication-* SSE transport values in FileInfo Debug. A reconciliation test pins that every key encryption_material_to_metadata produces is either transport-mapped or stripped. All four SSE replication e2e contracts still assert FAILED unchanged.
This commit is contained in:
@@ -27,9 +27,9 @@ const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-";
|
||||
const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-";
|
||||
const MINIO_INTERNAL_ENCRYPTION_PREFIX: &str = "x-minio-internal-server-side-encryption-";
|
||||
const MINIO_INTERNAL_ENCRYPTED_MULTIPART: &str = "x-minio-internal-encrypted-multipart";
|
||||
const RUSTFS_ENCRYPTION_ORIGINAL_SIZE: &str = "x-rustfs-encryption-original-size";
|
||||
const RUSTFS_ENCRYPTION_ORIGINAL_SIZE: &str = super::object_encryption_keys::INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER;
|
||||
const MINIO_ENCRYPTION_ORIGINAL_SIZE: &str = "x-minio-encryption-original-size";
|
||||
const SSEC_ORIGINAL_SIZE: &str = "x-amz-server-side-encryption-customer-original-size";
|
||||
const SSEC_ORIGINAL_SIZE: &str = super::object_encryption_keys::SSEC_ORIGINAL_SIZE_HEADER;
|
||||
|
||||
// Suffix constants (part after x-rustfs- or x-minio-). Use with get_header/insert_header.
|
||||
pub const SUFFIX_FORCE_DELETE: &str = "force-delete";
|
||||
|
||||
@@ -16,7 +16,9 @@ pub mod header_compat;
|
||||
pub mod headers;
|
||||
pub mod ip;
|
||||
pub mod metadata_compat;
|
||||
pub mod object_encryption_keys;
|
||||
pub use header_compat::*;
|
||||
pub use headers::*;
|
||||
pub use ip::*;
|
||||
pub use metadata_compat::*;
|
||||
pub use object_encryption_keys::*;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Canonical metadata keys persisted for encrypted objects and the replication
|
||||
//! transport mapping that carries SSE-C material between sites.
|
||||
//!
|
||||
//! The stored-key constants are the single source of truth shared by the SSE
|
||||
//! writer (`rustfs::storage::sse`), the replication boundary (`rustfs_ecstore`),
|
||||
//! and log redaction (`rustfs_filemeta`). Keys listed in
|
||||
//! [`SSEC_REPLICATION_TRANSPORT_HEADERS`] are renamed onto the wire for SSE-C
|
||||
//! ciphertext passthrough; every other encryption key must be stripped from
|
||||
//! outbound replication metadata via [`is_replication_stripped_encryption_key`].
|
||||
|
||||
use super::headers::{AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM, AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5};
|
||||
|
||||
pub const INTERNAL_ENCRYPTION_KEY_ID_HEADER: &str = "x-rustfs-encryption-key-id";
|
||||
pub const INTERNAL_ENCRYPTION_KEY_HEADER: &str = "x-rustfs-encryption-key";
|
||||
pub const INTERNAL_ENCRYPTION_IV_HEADER: &str = "x-rustfs-encryption-iv";
|
||||
pub const INTERNAL_ENCRYPTION_ALGORITHM_HEADER: &str = "x-rustfs-encryption-algorithm";
|
||||
pub const INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER: &str = "x-rustfs-encryption-original-size";
|
||||
pub const INTERNAL_ENCRYPTION_CONTEXT_HEADER: &str = "x-rustfs-encryption-context";
|
||||
pub const INTERNAL_ENCRYPTION_TAG_HEADER: &str = "x-rustfs-encryption-tag";
|
||||
pub const SSEC_ORIGINAL_SIZE_HEADER: &str = "x-amz-server-side-encryption-customer-original-size";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER: &str = "X-Minio-Internal-Encrypted-Multipart";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_IV_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Iv";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Kms-Sealed-Key";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Context";
|
||||
|
||||
pub const REPLICATION_SSEC_ALGORITHM_HEADER: &str = "X-Rustfs-Replication-Ssec-Algorithm";
|
||||
pub const REPLICATION_SSEC_KEY_MD5_HEADER: &str = "X-Rustfs-Replication-Ssec-Key-Md5";
|
||||
pub const REPLICATION_SSEC_ORIGINAL_SIZE_HEADER: &str = "X-Rustfs-Replication-Ssec-Original-Size";
|
||||
pub const REPLICATION_ENCRYPTION_IV_HEADER: &str = "X-Rustfs-Replication-Encryption-Iv";
|
||||
pub const REPLICATION_SSE_IV_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Iv";
|
||||
pub const REPLICATION_SSE_SEAL_ALGORITHM_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Seal-Algorithm";
|
||||
pub const REPLICATION_SSE_SEALED_KEY_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Sealed-Key";
|
||||
pub const REPLICATION_ENCRYPTED_MULTIPART_HEADER: &str = "X-Rustfs-Replication-Encrypted-Multipart";
|
||||
|
||||
/// Stored SSE-C metadata keys and the wire names they replicate under.
|
||||
///
|
||||
/// Source keys must match what `encryption_material_to_metadata` persists; the
|
||||
/// reconciliation test in `rustfs::storage::sse` pins that correspondence.
|
||||
pub const SSEC_REPLICATION_TRANSPORT_HEADERS: &[(&str, &str)] = &[
|
||||
(AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM, REPLICATION_SSEC_ALGORITHM_HEADER),
|
||||
(AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5, REPLICATION_SSEC_KEY_MD5_HEADER),
|
||||
(SSEC_ORIGINAL_SIZE_HEADER, REPLICATION_SSEC_ORIGINAL_SIZE_HEADER),
|
||||
(INTERNAL_ENCRYPTION_IV_HEADER, REPLICATION_ENCRYPTION_IV_HEADER),
|
||||
(MINIO_INTERNAL_ENCRYPTION_IV_HEADER, REPLICATION_SSE_IV_HEADER),
|
||||
(MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER, REPLICATION_SSE_SEAL_ALGORITHM_HEADER),
|
||||
(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, REPLICATION_SSE_SEALED_KEY_HEADER),
|
||||
(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER, REPLICATION_ENCRYPTED_MULTIPART_HEADER),
|
||||
];
|
||||
|
||||
/// Prefixes of replication SSE transport keys whose values carry encryption
|
||||
/// material and must never reach logs. Consumed by `rustfs_filemeta` redaction.
|
||||
pub const REPLICATION_SSE_TRANSPORT_PREFIXES: &[&str] = &[
|
||||
"x-rustfs-replication-server-side-encryption-",
|
||||
"x-rustfs-replication-encryption-",
|
||||
"x-rustfs-replication-ssec-",
|
||||
];
|
||||
|
||||
/// Maps a stored SSE-C metadata key to its replication transport name.
|
||||
pub fn ssec_replication_transport_header(stored_key: &str) -> Option<&'static str> {
|
||||
SSEC_REPLICATION_TRANSPORT_HEADERS
|
||||
.iter()
|
||||
.find(|(stored, _)| stored.eq_ignore_ascii_case(stored_key))
|
||||
.map(|(_, transport)| *transport)
|
||||
}
|
||||
|
||||
/// Returns true for metadata keys that must never leave the source site as
|
||||
/// plain replication metadata: encryption envelopes, SSE intent headers, and
|
||||
/// SSE-C material. SSE-C passthrough re-adds its keys through the transport
|
||||
/// mapping instead.
|
||||
pub fn is_replication_stripped_encryption_key(key: &str) -> bool {
|
||||
// The dual-key invariant writes an x-rustfs-internal- twin next to every
|
||||
// x-minio-internal- SSE key; cover it here so this predicate is safe to
|
||||
// use standalone, without an is_internal_key backstop.
|
||||
super::is_encryption_metadata_key(key)
|
||||
|| super::is_sse_header(key)
|
||||
|| key.eq_ignore_ascii_case(SSEC_ORIGINAL_SIZE_HEADER)
|
||||
|| super::starts_with_ignore_ascii_case(key, "x-rustfs-internal-server-side-encryption-")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transport_lookup_is_case_insensitive() {
|
||||
assert_eq!(
|
||||
ssec_replication_transport_header("X-AMZ-SERVER-SIDE-ENCRYPTION-CUSTOMER-ALGORITHM"),
|
||||
Some(REPLICATION_SSEC_ALGORITHM_HEADER)
|
||||
);
|
||||
assert_eq!(
|
||||
ssec_replication_transport_header("x-minio-internal-server-side-encryption-sealed-key"),
|
||||
Some(REPLICATION_SSE_SEALED_KEY_HEADER)
|
||||
);
|
||||
assert_eq!(ssec_replication_transport_header("x-rustfs-encryption-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stripped_predicate_covers_envelopes_intents_and_ssec_material() {
|
||||
// Managed-SSE envelope material (x-rustfs-encryption-* prefix).
|
||||
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_KEY_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER));
|
||||
// MinIO-internal sealed material, including the managed rio-v2 keys
|
||||
// that only a non-default feature build ever writes — pinning them
|
||||
// here keeps the default CI honest about the full key population.
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER));
|
||||
// The dual-key invariant's rustfs-internal twin must be covered
|
||||
// standalone, without relying on an is_internal_key backstop.
|
||||
assert!(is_replication_stripped_encryption_key(
|
||||
"x-rustfs-internal-server-side-encryption-sealed-key"
|
||||
));
|
||||
// SSE intent headers, including the KMS key id.
|
||||
assert!(is_replication_stripped_encryption_key("x-amz-server-side-encryption"));
|
||||
assert!(is_replication_stripped_encryption_key("x-amz-server-side-encryption-aws-kms-key-id"));
|
||||
assert!(is_replication_stripped_encryption_key(AMZ_SERVER_SIDE_ENCRYPTION_CUSTOMER_ALGORITHM));
|
||||
// is_sse_header does not cover the SSE-C original-size key; the
|
||||
// predicate must add it explicitly.
|
||||
assert!(is_replication_stripped_encryption_key(SSEC_ORIGINAL_SIZE_HEADER));
|
||||
assert!(is_replication_stripped_encryption_key(
|
||||
"X-Amz-Server-Side-Encryption-Customer-Original-Size"
|
||||
));
|
||||
// Ordinary user metadata passes through.
|
||||
assert!(!is_replication_stripped_encryption_key("x-amz-meta-app"));
|
||||
assert!(!is_replication_stripped_encryption_key("content-type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_prefixes_cover_every_transport_value_key() {
|
||||
// Every transport key that carries material must match a redaction
|
||||
// prefix; the multipart flag is a boolean marker and is exempt.
|
||||
for (_, transport) in SSEC_REPLICATION_TRANSPORT_HEADERS {
|
||||
if transport.eq_ignore_ascii_case(REPLICATION_ENCRYPTED_MULTIPART_HEADER) {
|
||||
continue;
|
||||
}
|
||||
let lower = transport.to_lowercase();
|
||||
assert!(
|
||||
REPLICATION_SSE_TRANSPORT_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| lower.starts_with(prefix)),
|
||||
"transport key {transport} is not covered by a redaction prefix"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user