feat(replication): replicate managed-SSE objects via target re-encryption (#5885)

Open the managed-SSE replication gate (backlog#1783, PR-B of 3, after
#5872): the replication reader already decrypts through the injected
object-encryption resolver, so the source sends plaintext plus an
encryption intent header (AES256 / aws:kms, never the source key id) and
the target re-encrypts on its normal PUT path with its own KMS. No DEK
crosses sites.

- replication_put_object_options: fail closed only on Unsupported;
  insert the SSE intent after the strip loop.
- TargetClient::create_multipart_upload sends the full opts.header()
  set, fixing multipart replicas losing content-type/user metadata
  (plaintext included).
- Preserve source ETag and mtime on replicas (authorized replication
  only): receiver wires x-rustfs-source-etag into preserve_etag for PUT
  and CompleteMultipartUpload, resolve_complete_etag consumes it, and
  complete options carry source_etag/source_mtime (absent mtime
  degrades to epoch, not now_utc). Without this every replication HEAD
  comparison re-drives re-encrypted objects forever.
- e2e: managed SSE contracts flip to success on an independent-KMS
  dual-process pair (byte-identical plain GET proves target-owned
  envelopes; ETag/mtime preserved; version stable across scanner
  cycles; resync converges; multipart keeps structure and metadata);
  new target-without-KMS fail-closed contract; SSE-C stays FAILED.

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
唐小鸭
2026-08-09 18:21:35 +08:00
committed by GitHub
parent a71726ef49
commit 73e4ef4dd4
8 changed files with 408 additions and 125 deletions
@@ -1922,14 +1922,11 @@ impl TargetClient {
object: &str,
opts: &PutObjectOptions,
) -> Result<String, S3ClientError> {
let mut headers = HeaderMap::new();
// Object metadata belongs to CreateMultipartUpload in S3 semantics;
// building only the source-version headers here used to drop user
// metadata, content-type, and the SSE intent for multipart replicas.
let headers = opts.header();
let version_id = opts.internal.source_version_id.clone();
if !version_id.is_empty() {
insert_header(&mut headers, SUFFIX_SOURCE_VERSION_ID, &version_id);
}
if opts.internal.replication_request {
insert_header(&mut headers, SUFFIX_SOURCE_REPLICATION_REQUEST, "true");
}
// The remote version of a multipart replication is decided at initiate
// time; CompleteMultipartUpload does not read a versionId.
let api_version_id = resolve_put_api_version_id(&version_id).map(ToOwned::to_owned);