spetz commented on code in PR #4615:
URL: https://github.com/apache/iggy/pull/4615#discussion_r4337737015


##########
core/partitions/src/iggy_partitions.rs:
##########
@@ -799,8 +833,13 @@ where
         // user headers per message. The ciphertext is what gets journaled,
         // replicated, checksummed, and persisted -- every replica stores
         // identical bytes -- and the poll reply is the single decrypt point.
+        // The partition's own encryptor: each stream has its own key, pinned
+        // to the partition when it was built.
+        let encryptor = self

Review Comment:
   Fixed. `on_request_with_reply` does one `get_mut_by_ns` lookup 
(`core/partitions/src/iggy_partitions.rs:836`) and encrypts through 
`partition.encryptor()` by reference 
(`core/partitions/src/iggy_partitions.rs:860`), so there is no second lookup 
and no `Arc` clone per batch. The partitions lib suite passes unchanged.



##########
core/server/src/boot/mod.rs:
##########
@@ -911,6 +911,26 @@ async fn shard_main(
         None
     };
 
+    // Stream key rewrapper: shard 0 only, and only when this node holds a
+    // server key. Each pass no-ops unless this node is the caught-up metadata
+    // primary, so a key rotation is replicated once to every replica.
+    let stream_key_rewrapper_stop =
+        if shard_id == 0 && 
shard.plane.partitions().key_context().accepts_stream_keys() {

Review Comment:
   Fixed. The rewrapper starts only on a node with previous keys or retired 
fingerprints (`core/server/src/boot/mod.rs:930`), and a tick returns at once 
unless this node is the caught-up primary and metadata moved since the last 
sweep (`core/server/src/stream_key_rewrapper.rs:128`). 
`given_unchanged_metadata_when_sweeping_again_should_skip_the_scan` pins the 
skip.



##########
core/server/src/boot/recovery.rs:
##########
@@ -127,16 +142,38 @@ pub(in crate::boot) async fn build_shard_for_thread(
     let owned_partitions_capacity = total_partitions
         .div_ceil(usize::from(total_shards).max(1))
         .saturating_mul(2);
-    // At-rest encryption: built once per shard from the shared config; the
-    // ingestion path encrypts on the primary and the poll reply decrypts.
-    // A bad key fails the boot rather than silently serving plaintext.
-    let encryptor = if config.encryption.enabled {
-        let aes = Aes256GcmEncryptor::from_base64_key(&config.encryption.key)
-            .map_err(|error| ServerError::Iggy(Box::new(error)))?;
-        Some(Arc::new(EncryptorKind::Aes256Gcm(aes)))
-    } else {
-        None
-    };
+    // Each encrypted stream holds its own key, wrapped under the server key.
+    // Every one must open here before anything serves, so a changed config key
+    // stops the boot instead of failing sends and polls one by one.
+    let key_context = Arc::new(
+        ServerKeyContext::from_config(
+            &config.encryption.key,
+            &config.encryption.previous_keys,
+            config.encryption.enabled,
+        )
+        .map_err(|error| ServerError::Iggy(Box::new(error)))?,
+    );
+    let unopenable = metadata.mux_stm.streams().read(|inner| {
+        inner.items.iter().find_map(
+            |(_, stream)| match StreamEncryption::from(&stream.options) {
+                StreamEncryption::Plaintext => None,
+                StreamEncryption::Unavailable(error) => Some((stream.id, 
Vec::new(), error)),
+                StreamEncryption::Wrapped(set) => key_context
+                    .open(&set)
+                    .err()
+                    .map(|error| (stream.id, set.kek_fingerprints(), error)),
+            },
+        )
+    });
+    if let Some((stream_id, stored_kek_fingerprints, source)) = unopenable {
+        return Err(ServerError::StreamKeyUnavailable {

Review Comment:
   Fixed. A cluster replica whose stream key does not open logs both 
fingerprint sets and leaves that stream's partitions to the reconciler instead 
of refusing boot (`core/server/src/boot/recovery.rs:253`), and a standalone 
replay still refuses. 
`given_replica_that_missed_a_rotation_when_booting_with_the_new_key_should_serve_after_catching_up`
 pins it.



##########
core/server/src/boot/recovery.rs:
##########
@@ -204,8 +248,20 @@ pub(in crate::boot) async fn build_shard_for_thread(
     // bundle was broadcast (see `MetadataHandoff::Owner`). All shards
     // here only add their per-partition deltas, so the shared
     // `Arc<TopicStats>` atomics race only against other atomic adds.
-    for (stream_id, topic_id, partition_stats, partition_metadata, 
topic_runtime) in owned {
+    for OwnedPartition {
+        stream_id,
+        topic_id,
+        stats: partition_stats,
+        metadata: partition_metadata,
+        runtime: topic_runtime,
+        identity,
+        encryption,
+    } in owned
+    {
         let namespace = IggyNamespace::new(stream_id, topic_id, 
partition_metadata.id);
+        let encryptor = partitions

Review Comment:
   Fixed. Only shard 0 runs the boot-wide check, and only for encrypted streams 
that own no partition (`core/server/src/boot/recovery.rs:159`). Every other set 
is opened once by each shard that owns one of its partitions 
(`core/server/src/boot/recovery.rs:216`). Keyrings stay per shard, so no 
`Arc<EncryptorKind>` is shared across cores.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to