Repository navigation
Description
Hello, I have the following setup and use case:
- Source: A server at a secure-ish location w/o encryption
- Pull destination: A NAS at a shared location with native encryption enabled at
pool/backup. - The source's
tank/aandtank/a/b/care replicated, buttank/a/bis not. Zrepl will create a placeholder for it.
Expected behavior: All files under pool/backup are encrypted and the received filesystems inherit pool/backup's keys.
Actual behavior: Zrepl explicitly disabled encryption for tank/a/b, causing unencrypted data to be written/leaked onto the destination's hardware. Eek!
Indeed it is impossible to have tank/a/b/c encrypted because the inheritance chain is broken by the placeholder. Setting encrypting properties explicitly can work, but is clunky at best. Setting encryption=aes-256-gcm in properties.inherit at least makes the whole thing fail instead of leaking unencrypted data.
Analysis and Source-Level Fix
I found that this behavior was intentionally introduced in commit 8ff83f2 to serve issue #342. IMO, that behavior is dangerous. Please never disable my encryption automatically :-(.
Removing lines 92-96 in zfs/placeholder.go resolves the issue: The placeholder is created with default properties, inherits its encryption, and the plain send stream is re-encrypted like expected.
Recommendation to fix:
Make placeholder properties configurable :-). Afterwards, drop the hardcoded and dangerous encryption=off completely.
If the user expects an encrypted send stream, then the user is free to set e.g. placeholders.properties = {encryption: off} in their job. This info would nicely fit into existing docs regarding encrypted sending. Otherwise, placeholders simply inherit their properties and are thus free of surprises.
Bonus: canmount and similar properties become configurable with placeholders for free.
Configs
Source:
- name: foo
type: source
serve:
type: tcp
listen: 10.42.1.1:8888
clients:
10.42.1.32: example
filesystems:
"tank/a<": true
"tank/a/b": false
"tank/a/b/c<": true
snapshotting:
type: periodic
interval: 15m
prefix: zrepl_
Pull:
jobs:
- name: bar
type: pull
root_fs: pool/backup
recv:
properties:
override:
canmount: noauto
mountpoint: none
inherit:
- keylocation
- keyformat
- encryption
connect:
type: tcp
address: 10.42.1.1:8888
interval: 15m
pruning:
keep_sender:
- type: not_replicated
- type: last_n
count: 10
- type: grid
grid: 1x1h(keep=all) | 24x1h | 30x1d
regex: "zrepl_.*"
- type: regex
regex: "^manual_.*"
keep_receiver:
- type: grid
grid: 1x1h(keep=all) | 24x1h | 30x1d | 3x30d
regex: "zrepl_.*"
Activity
problame commentedon Aug 6, 2021
Thanks for this bug report and apologies for not getting back to this sooner.
It is really unfortunate that we did not revisit the placeholder's
encryptionoverride when we introduced support for encrypted sends.What we actually want, but didn't get, is an
-o encryption=off but don't inherit thatflag.Which doesn't exist.
Your workaround of removing the hardcoded
-o encryption=offfor placeholders is viable.And I like your subsequent proposal even better:
Good idea.
However: IIRC the problem with encrypted placeholders is that we can't
zfs recv -Fthem in-place (it's a limitation of ZFS native encryption). The code that "handles" that case is this beauty here:zrepl/endpoint/endpoint.go
Lines 790 to 874 in 009bd41
If, after an successful initial replication, a user replaces a placeholder with the real dataset in the sender's
filesystemsfilter, then they'll have to juggle around datasets themselves, as described in the error messages.I guess that's a trade-off that we'll just have to live with. (cc @InsanePrawn @Ranlvor )
WIP: fix encrypt-on-recv
mologie commentedon Aug 9, 2021
Appreciate the response. This looks like an unfortunate limitation of OpenZFS indeed, I was not aware of it.
Do you happen to have an OpenZFS ticket to link to this one and explain our use case? I cannot find a mention of this limitation in
man zfs-recv, nor do I find open bugs under the termsrecv F. Chances are that this is not intended/documented behavior.problame commentedon Aug 12, 2021
In the commit linked above I played around with an alternative approach to handling the situation implicitly on the receiver.
The advantage is that it doesn't leak the concept of placeholders to the user.
The disadvantage is that we cannot cleanly disambiguate the following two use cases:
send.encrypted = true) and the receiver doesn't know / load the key for the received datasets.encryptionetc. fromroot_fsor a parent thereof. (That's the use case that prompted this issue).This could be addressed by requiring the user to indicate their intention explicitly in the recv-side config, e.g., through an option
recv.encryped = false | true | re-rencrypt-from-root_fs.If
recv.encrypted=re-rencrypt-from-root_fs, zrepl would then make sure that only unencrypted sends are accepted, thatroot_fs'skeystatus=available, and placeholders created without-o encryption=off.Does that sound better or worse than leaking the concept of placeholders to the user?
NB: the receiver will know whether the send is encrypted or not based on Tbd metadata that the sender adds to the
RecvRequestproto message . The receiver can trust the sender on this because, if the sender lies about the encryptedness of the send stream, it only hurts them, not the receiver.mologie commentedon Aug 13, 2021
I like explicit better than implicit, so indicating whether I expect an unencrypted stream and want it re-encrypted sounds reasonable to me. This is better indeed than leaving configuration of what in the end is an implementation detail to the user, like I would originally have suggested.
problame commentedon Aug 13, 2021
Then the last open question is: how do we deal with existing configs & recv-side ZFS state under
root_fs? What is the default forrecv.encrypted?Also, ideally we want to automatically notify existing users that are affected by this issue once they upgrade (while not breaking their setup(?)).
mologie commentedon Aug 22, 2021
Apologies for letting this sit unanswered.
I am afraid I have no user-friendly suggestion for your question. It /may/ be reasonable here to break things and have the user decide how to resume operation when a placeholder with encryption=off exists under an encrypted root_fs from a previous zrepl version.
The user's options are to either purge the affected fs and re-sync with recv.encrypted=re-rencrypt-from-root_fs, or explicitly set recv.encrypted=off and thus keep the previous version's behavior.
None of the options
recv.encryped = false | true | re-rencrypt-from-root_fsdiscussed so far seem to work as default. The sender by default sends an unencrypted stream, sore-rencrypt-from-root_fswould be closest to what I would pick as default, except that it breaks existing workflows due to thekeystatus=availableconstraint.Let's reevaluate possible values of recv.encrypted; false, true, and re-encrypt IMO leave too much room for interpretation and do not work as default. Hypothetically speaking, do I set it to true when receiving an encrypted stream, or when I want the received stream stored in encrypted state? What is the difference between true and re-encrypt?
How about
inherit-from-root_fsinstead ofre-rencrypt-from-root_fs, dropping thekeystatus=availableconstraint? Afterwards, my understanding of your option choices is that we could replacetruebyraw(= I expect to receive a raw encrypted stream), and renamefalseto something more explicit that indicates that encrypt will only be disabled at and below placeholders.problame commentedon Aug 23, 2021
Hm, how about
unencrypted-o encryption=offsender_encrypted-o encryption=offencrypt_with_rootfs_encryption_root-oencryptionrootis notroot_fsor one ofroot_fs's parentsencryptionrootis notroot_fsor one ofroot_fs's parentsPeople who use
send.raw=truewith a mixed set of encrypted and unencrypted filesystems will observe "undefined behavior" / "strange errors" for the time being. (cc @infinisil is that a problem for your use case? I don't know other people who usesend.raw=true).mologie commentedon Aug 23, 2021
Sounds rather complicated to me. Not sure if I understood this correctly, but would we not achieve the same functionality by just mirroring the
rawproperty of the sender? After all, that is all that seems to be relevant for this issue, everything else is encryption sanity checking that I'd rather move into a separate property.recv.raw = true | false(default: false)true: placeholders with-o encryption=offfalse: placeholder properties are inherited, fails if send stream is encrypted/rawAdditionally, an optional property
recv.encrypted = true | false(default: false) could contain the encryption root validation described by you above. If enabled, zrepl would refuse to store an unencrypted recv stream on an unencrypted device (w/o re-encryption).Notification mechanism about this issue: Fail if placeholders with
encryption=offexist butraw = false(default).problame commentedon Aug 23, 2021
I like the idea of untangling the two aspects of
placeholdersandencryption root validation.But I think
recv.rawoverloads therawterm - it's easy to confuse withsend.raw.Which are not the same thing according to the behavior that you specified above.
I think that, in essence,
recv.rawis only slightly more abstract thanrecv.placeholders.encryption = off | inherit. So maybe we should leak the concept to the user after all?mologie commentedon Aug 30, 2021
That was pretty much exactly my point -- set them to the same thing on both sides and zrepl does the right thing automatically. It is an abstraction after all.
Either way we have come full circle :). Whether it's a
recv.placeholders.encryption = off | inheritoption or abstraction/magic underrecv.rawseems to be a cosmetic and style choice at this point. The values mostly map 1:1 to each other.I like explicit better than implicit, so if I had to pick I would go for an explicit
recv.placeholders.encryptionand explain the reason for requiring configuration here to the user in zrepl's docs. Zrepl can also notify affected users by erroring out if it observes a placeholder withencryption=offdespiterecv.placeholders.encryptionbeing undefined.problame commentedon Sep 25, 2021
Ok, I'm convinced. @InsanePrawn thoughts?
InsanePrawn commentedon Sep 29, 2021
Sounds reasonable.
The implicit default value of
recv.placeholders.encryptionwould beinherit(ing the encryption root from its recv-side parent) to matchsend.raw = off, right?7 remaining items
fix encrypt-on-receive with placeholders
fix encrypt-on-receive with placeholders
fix encrypt-on-receive with placeholders
squash into "fix encrypt-on-receive with placeholders"
squash into "fix encrypt-on-receive with placeholders"
problame commentedon Dec 9, 2021
problame/developas of 79af285 contains the change that makes initial replications work without specifyingrecv.placeholder.encryptionif they don't have any gaps on the sending side filter.@mologie @razielgn are you interested in testing this? I have added a bunch of platformtests, that should keep surprises at bay. I think it's safe to upgrade the daemon, and if things break, just switch back the binary.
razielgn commentedon Dec 9, 2021
@problame Sure! I should be able to give it a try this weekend. Thanks for the effort!
razielgn commentedon Dec 12, 2021
@problame Ok I tested the commit with this config:
Vol
test-sink/zrepl-encrypted-sinkwas created usingzfs create -o encryption=on -o keyformat=passphrase -o keylocation=prompt test-sink/zrepl-encrypted-sink.After zrepl has finished syncing, I could see that all created subvolumes reported encryption=on using
zfs get -r encryption test-sink/zrepl-encrypted-sinkproblame commentedon Dec 12, 2021
To be expected since
nas-test-backups.filesystemshas gaps.Was the error message sufficiently helpful?
Great 👍
razielgn commentedon Dec 12, 2021
Yeah the error message immediately guided me into the right direction. Thanks for taking the time and implementing this patch!
problame commentedon Dec 12, 2021
Glad it's working. Hope to release a 0.5-rc1 soon.
fix encrypt-on-receive with placeholders