Conversation
3166d6c to
3d534d6
Compare
|
@ggershinsky curious to hear your thoughts on above. Specifically the questions I left in the PR description. Thanks! |
|
sgtm. manifest list file keys are generated per snapshot, and are not re-used. if a snapshot is removed, no reason to keep its key. |
|
sounds good. @huaxingao could I get a review on the above please! |
|
friendly ping on above! Sorry still new 😅 @ggershinsky would appreciate a review and if you could tag in someone else who could give this a review and is familiar with the encryption space! |
|
sure, maybe @huaxingao and @singhpk234 can help |
|
Hey @huaxingao / @singhpk234 friendly ping on above 😄 |
singhpk234
left a comment
There was a problem hiding this comment.
The change makes sense to me ! can we add an E2E where post removing the expired snapshot (MLK) one is still able to read the table ?
- While adding E2E test found a bug in the HiveTableOperations. - HiveTableOperations, as well as the currently open PR for the RestTableOperations, adds all keys from EncryptionManager into TableMetadata
|
@Hugo-WB Could you resolve the conflict? |
Hugo-WB
left a comment
There was a problem hiding this comment.
Thanks for the review @huaxingao!
Merged upstream/main and updated a comment.
I still have some concerns about the correctness here long term. Left a comment, would be keen to get the answer to it before merging. Thanks!
| Set<String> referencedEncryptionKeys = | ||
| Sets.union( | ||
| metadata.snapshots().stream() | ||
| .map(Snapshot::keyId) | ||
| .filter(Objects::nonNull) | ||
| .collect(Collectors.toUnmodifiableSet()), | ||
| metadata.encryptionKeys().stream() | ||
| .map(EncryptedKey::encryptedById) | ||
| .filter(Objects::nonNull) | ||
| .collect(Collectors.toUnmodifiableSet())); | ||
| TableMetadata.Builder metadataBuilder = TableMetadata.buildFrom(metadata); | ||
| standardEncryptionManager.encryptionKeys().values().stream() | ||
| .filter( | ||
| encryptedKey -> | ||
| referencedEncryptionKeys.contains(encryptedKey.keyId()) | ||
| // The KEK may not be referenced but must still be saved in the metadata. | ||
| || standardEncryptionManager.keyEncryptionKeyID().equals(encryptedKey.keyId())) | ||
| .forEach(metadataBuilder::addEncryptionKey); | ||
| return metadataBuilder.build(); |
There was a problem hiding this comment.
This new logic depends on an invariance that the manifestListKey -> KEK -> table encryption key. are all one "hop". E.g. the -> means encryptedBy.
If there was a world where a key in the encryptionKeys() list is encrypted by another key in the encryptionKey list, which is then in itself encrypted by the KEK, and they were all added in a single commit, this PR would fail to propagate the middle key.
E.g.:
EncryptionKeys:[{key: A, encryptedBy: B}, {key: B, encryptedBy: C}, {key: C, encryptedBy: KEK}]
snapshot: [snapshotId: x, keyId: A]
And all of the above happens within one commit.
In the above code, we would not add key B to the metadata file. As none of the encryptionKeys will be part of metadata.encryptionKeys() as that will be empty. We will keep A and C, since A is referenced by snapshot and C is a KEK.
TLDR: question @ggershinsky : we expect the depth of encryption key referencing from manifest list -> KEK to remain at a single hop right, we don't expect there to be another hop?
There was a problem hiding this comment.
On second thought a behaviour change here would get caught by e2e tests. But worth noting.
There was a problem hiding this comment.
yep, the approach is direct - to save KMS interactions, ml keys are encrypted by an intermediate local KEK, that in turn is encrypted by KMS. So it's always ml key -> KEK -> KMS. No reason to introduce another hop.
| } else { | ||
| tableMetadata = metadata; | ||
| } | ||
| tableMetadata = EncryptionUtil.addEmKeysToMetadata(metadata, encrManager); |
There was a problem hiding this comment.
cc @smaheshwar-pltr for: #13225. If you could update your REST implementation to use this utility once it merges.
|
Friendly ping @huaxingao @singhpk234 |
|
Apologies for the ping, but another friendly ping here! 😄 @huaxingao @singhpk234 |
|
@singhpk234 Do you have more comments on this PR? If not, I will merge. |
| SparkActions.get() | ||
| .expireSnapshots(table) | ||
| .expireSnapshotId(snapshotToExpire.snapshotId()) | ||
| .execute(); |
There was a problem hiding this comment.
minor/can be done in follow-up: its would be nice to assert that this snapshot is actually expired as well ?
|
Friendly ping @huaxingao @singhpk234, if this is still good to merge 🙏 |
gaborkaszab
left a comment
There was a problem hiding this comment.
Thank you for the PR, @Hugo-WB !
I've bumped into this from a different angle: I'm about to add encryption to statistics files and I was wondering how to do the encryption key cleanup. Thx @ggershinsky to pointing me to this PR.
I think there might be an issue with the approach in some use-cases, probably not that complicated to fix. See in my comment.
| } | ||
| removeStatistics(snapshotId); | ||
| removePartitionStatistics(snapshotId); | ||
| removeEncryptionKey(snapshot.keyId()); |
There was a problem hiding this comment.
I gave the PR to Claude and it might have found an issue with the current design:
suppressHistoricalSnapshots() method on TableMetadata.Builder may collide with the approach here. Let's say we create TableMetadata like below setting up lazy snapshot loading (considering 2 snapshots, a current and a historical one):
TableMetadata lazyMetadata =
TableMetadata.buildFrom(metadata)
.suppressHistoricalSnapshots()
.setSnapshotsSupplier(() -> ImmutableList.of(historical, current))
.discardChanges()
.build();
It will have only the referenced snapshots and omit the rest. Due to the current implementation it removes encryption keys for the unloaded snapshots. When we load the rest of the snapshots through snapshotSupplier we won't have encryption keys for all the snapshots, so time travel to such snapshots would fail.
Whether this is a realistic scenario or not, I can't judge, but you can repro this through the public API of the TableMetadata builder.
WDYT?
There was a problem hiding this comment.
Hey! Sorry for the slowness here. Thanks for the review!
I think this is a valid point! And definitely does break the current implementation. It also does seem to be used here, in the exact case where SnapshotMode::REFS, so the rest catalog would've ran suppressHistoricalSnapshots.
Downside here is that SnapshotMode::REFS will now return a lot of the encryption keys 😭 and we are not necessarily reducing the size of the loadtable RPC.
I do feel setSnapshotsSupplier's API does seem to be a bit at odds with the way encryption keys are set up within the TableMetadata 😅 . If the goal of it is to supply snapshots that aren't currently within metadata, we should figure out how it can also supply encryption keys for those snapshots. Curious as to why manifest list encryption keys are stored at the metadata layer and not at the snapshot layer.
I've moved the removeEcnryptionKey into !suppress block so that we don't remove encryption keys for now. Lmk what you think!
Updated: Hugo-WB@d78c752
There was a problem hiding this comment.
Thank you for getting back with this @Hugo-WB !
I think encryption keys is not any different than schemas or partition specs for instance. When we suppress snapshots to REFS we still need the complete list of schemes and specs in case we want to load later on the missing snapshots (suppress mode). When expiring snapshot, if no remaining snapshot uses them, they can be removed (!suppress mode).
One thing I think still breaks here: I'm not entirely sure we can assume that the mapping between snapshots and encryption keys is 1 to 1. In the reference implementation it seems to be the case, but there might be custom implementations where this isn't true.
To make this more general, I think we should also account the snapshots retained and their key IDs. Maybe adding the change not here but to RemoveSnapshots.internalApply()?
There was a problem hiding this comment.
Agreed! In the current spec encryption keys are stored at the metadata layer, similar to schemas/partition specs. I think this makes sense for the KEK, however for manifest list encryption key, those are per snapshot and not re-used, so would've been nice to have them per snapshot.
As you mention above having the keys in the snapshot itself would also make the invariant of 1:1 snapshot <> encryption key more obvious.
Yep! I had this PR open that I was planning on opening up after this one merged: https://github.com/apache/iceberg/pull/16395/changes which creates the list of references keys and removes all unreferenced.
Agreed on the invariance, I mentioned it in the PR description. It seems like things that are 1:1 with snapshot are removed at TableMetadata::rewriteSnapshotsInternal and things that are shared across snapshots (schemas/partitions etc...) RemoveSnapshots::internalApply::cleanExpiredMetadata.
If we rely on this invariance, which I think people are generally agreed with relying on this invariant/it holding in the long run (Mentioned it in PR description and didn't get any disagreements)? I think it makes sense to remove encryption keys as part of TableMetadata::rewriteSnapshotsInternal.
Thanks! Curious on your thoughts.
There was a problem hiding this comment.
I think the point of having encryption keys in TableMetadata and not per snapshot is to enable reusing those keys across files. The current reference implementation might not work that way, but a custom encryption manager/KMS might do. So I don't think we can expect 1:1 relation between snapshots and encryption keys. That's why I proposed to move removing keys to RemoveSnapshots.
There was a problem hiding this comment.
👍 I see. If we're in favor of not relying on this invariant, I agree this PR should not be implemented:
Curious for your thoughts/review on: #18102 then!
Will defer to others that are more familiar with the original motivations behind making snapshot encryption keys stored in the snapshot metadata on whether we should rely on this invariant of snapshot -> key being 1:1.
I think it would be nice if encryption keys were removed with the snapshot and not require expire_snapshots with cleanExpiredMetadata.
Curious for thoughts from @ggershinsky and @singhpk234
There was a problem hiding this comment.
motivations behind making snapshot encryption keys stored in the snapshot metadata on whether we should rely on this invariant of snapshot -> key being 1:1.
Today, there are two kinds of entries in the TableMetadata "encrypted keys" list. One is the manifest list keys, and the other is key encryption keys (the former are encrypted by the latter). Key encryption keys are re-used for a long time, but still should be cleaned up after all manifest list keys, encrypted by them, are removed.
There was a problem hiding this comment.
(the condition for removal of a key encryption key - all of its ML keys are removed, and its lifetime has expired)
There was a problem hiding this comment.
I'm quoting from @rdblue , let me know if I misunderstand: "the keys table is intended to have one or two keys that are reused, not a key per encrypted file referenced from the table metadata."
For me this means, that theoretically we can have a 1-many relationship between keys and manifest lists (or whatever else's key we store in the encrypted keys list).
The entire conversation.
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
|
This pull request has been closed due to lack of activity. This is not a judgement on the merit of the PR in any way. It is just a way of keeping the PR queue manageable. If you think that is incorrect, or the pull request requires review, you can revive the PR at any time. |
|
Hey @huaxingao, sorry for letting this close, was out of office for a while. Could we please re-open this PR given the interest in the mailing list. thanks! |
|
@singhpk234 as well if we're still happy to merge this 😄 |
gaborkaszab
left a comment
There was a problem hiding this comment.
Hi @Hugo-WB ,
Thanks for resuming the work on this! I can't reopen the PR but some committer can do so for you.
I think there are some conceptual questions still, I left some comments.
| } | ||
| removeStatistics(snapshotId); | ||
| removePartitionStatistics(snapshotId); | ||
| removeEncryptionKey(snapshot.keyId()); |
There was a problem hiding this comment.
Thank you for getting back with this @Hugo-WB !
I think encryption keys is not any different than schemas or partition specs for instance. When we suppress snapshots to REFS we still need the complete list of schemes and specs in case we want to load later on the missing snapshots (suppress mode). When expiring snapshot, if no remaining snapshot uses them, they can be removed (!suppress mode).
One thing I think still breaks here: I'm not entirely sure we can assume that the mapping between snapshots and encryption keys is 1 to 1. In the reference implementation it seems to be the case, but there might be custom implementations where this isn't true.
To make this more general, I think we should also account the snapshots retained and their key IDs. Maybe adding the change not here but to RemoveSnapshots.internalApply()?
| } else { | ||
| tableMetadata = metadata; | ||
| } | ||
| tableMetadata = EncryptionUtil.addEmKeysToMetadata(metadata, encrManager); |
There was a problem hiding this comment.
I'm not entirely sure what problem this part of the PR tries to solve. I had the impression that the current changes in TableMetadata are meant to cover the scope of the PR: "Clean up encryption keys as part of RemoveSnapshots".
This seems like another mechanism to clean up unreferenced encryption keys when committing to a table. If I'm not mistaken here with my assumption, then this is orthogonal and should be covered separately.
There was a problem hiding this comment.
Sorry should've added a PR comment. Without this change the tests in TestTableEncryption would not pass. The previous HiveTableOperations constructed the EncryptionManager from the old metadata, then added all old encryption keys to the new metadata. Thus it was impossible to ever actually remove encryption keys.
Agree this is very confusing. Happy to discuss separately. But since EncryptionManager stores it's own state separately to TableMetadata, it needs to be kept in sync with the TableMetadata.
Fixes the first part of: #16352 for
ExpireSnapshotsBy calling
removeEncryptionKeywhen we remove snapshot from metadata.This assumes that snapshot <> encryption key is 1:1. I think this is a valid assumption given a new encryption key is created per manifest list here? But keen to get thoughts from others on this invariant, I am relatively unfamiliar with all this. Do people know what the intention here was? Are there cases where key re-use could happen?