backups: zfs: fix encryption handling post OOB restore

This commit is contained in:
hayzamjs
2026-05-21 14:04:05 +05:30
parent 32f79e779a
commit 9a1b590aae
10 changed files with 347 additions and 48 deletions
+47 -6
View File
@@ -121,17 +121,58 @@ func (s *Service) EnsureEncryptionKeyFile(uuid string) error {
return nil
}
func (s *Service) ensureEncryptionKeyForDataset(ctx context.Context, ds *gzfs.Dataset) error {
func (s *Service) ensureEncryptionKeyForDataset(ctx context.Context, ds *gzfs.Dataset) (keyLoaded bool, err error) {
keylocProp, err := ds.GetProperty(ctx, "keylocation")
if err != nil {
return fmt.Errorf("get_keylocation_failed: %w", err)
return false, fmt.Errorf("get_keylocation_failed: %w", err)
}
keyloc := strings.TrimSpace(keylocProp.Value)
if keyloc == "" || keyloc == "none" || !strings.HasPrefix(keyloc, "file://") {
return fmt.Errorf("unexpected_keylocation: %s", keyloc)
if keyloc == "" || keyloc == "none" {
return false, nil
}
uuid := filepath.Base(strings.TrimPrefix(keyloc, "file://"))
return s.EnsureEncryptionKeyFile(uuid)
if strings.HasPrefix(keyloc, "file://") {
uuid := filepath.Base(strings.TrimPrefix(keyloc, "file://"))
if err := s.EnsureEncryptionKeyFile(uuid); err != nil {
return false, fmt.Errorf("encryption_key_not_found_in_cluster_store: %s", uuid)
}
if err := ds.LoadKey(ctx, false); err != nil {
return false, fmt.Errorf("load_key_failed: %w", err)
}
return true, nil
}
// keylocation is "prompt" — the original key file wasn't available on
// the server that received this dataset (e.g. a backup target). Try each
// key in the cluster store; match by attempting to load.
keys, listErr := s.Cluster.ListEncryptionKeys()
if listErr != nil {
logger.L.Warn().Err(listErr).Str("dataset", ds.Name).
Msg("encrypted_dataset_cannot_list_keys_for_prompt_keylocation")
return false, nil
}
for _, key := range keys {
if err := ds.LoadKeyWithPassphrase(ctx, key.KeyData); err != nil {
continue
}
// Key loaded — materialize the key file and fix keylocation
// so it auto-loads on next boot.
if writeErr := s.EnsureEncryptionKeyFile(key.UUID); writeErr != nil {
logger.L.Warn().Err(writeErr).Str("uuid", key.UUID).
Str("dataset", ds.Name).Msg("prompt_key_loaded_but_file_write_failed")
} else {
_ = ds.SetProperties(ctx, "keylocation", "file://"+filepath.Join(EncryptionKeyDirectory, key.UUID))
}
logger.L.Info().Str("dataset", ds.Name).Str("uuid", key.UUID).
Msg("encrypted_dataset_key_loaded_via_prompt_fallback")
return true, nil
}
logger.L.Warn().Str("dataset", ds.Name).
Msg("encrypted_dataset_requires_manual_key_load_run_zfs_load_key")
return false, nil
}
+5 -4
View File
@@ -3707,11 +3707,12 @@ func (s *Service) prepareReplicatedDatasetForActivation(ctx context.Context, roo
}
if ds.IsEncrypted() {
if err := s.ensureEncryptionKeyForDataset(ctx, ds); err != nil {
return fmt.Errorf("replication_encryption_key_missing_%s: %w", dataset, err)
keyLoaded, err := s.ensureEncryptionKeyForDataset(ctx, ds)
if err != nil {
return fmt.Errorf("replication_encryption_key_failed_%s: %w", dataset, err)
}
if err := ds.LoadKey(ctx, false); err != nil {
return fmt.Errorf("replication_load_key_failed_%s: %w", dataset, err)
if !keyLoaded {
logger.L.Warn().Str("dataset", dataset).Msg("replication_encryption_key_not_auto_loaded")
}
}
+5 -5
View File
@@ -358,13 +358,13 @@ func (s *Service) fixRestoredProperties(ctx context.Context, dataset string) {
}
if ds.IsEncrypted() {
if err := s.ensureEncryptionKeyForDataset(ctx, ds); err != nil {
logger.L.Error().Err(err).Str("dataset", dataset).Msg("fix_restored_property_key_missing")
keyLoaded, err := s.ensureEncryptionKeyForDataset(ctx, ds)
if err != nil {
logger.L.Error().Err(err).Str("dataset", dataset).Msg("fix_restored_property_key_load_failed")
return
}
if err := ds.LoadKey(ctx, false); err != nil {
logger.L.Error().Err(err).Str("dataset", dataset).Msg("fix_restored_property_load_key_failed")
return
if !keyLoaded {
logger.L.Warn().Str("dataset", dataset).Msg("fix_restored_property_key_not_auto_loaded")
}
}
@@ -24,6 +24,7 @@
import { vmBaseDataset, vmStoragePools } from '$lib/utils/vm/vm';
import { watch } from 'runed';
import { toast } from 'svelte-sonner';
import { sleep } from '$lib/utils';
interface Props {
open: boolean;
@@ -172,11 +173,18 @@
}
srcEncryptionChecking = true;
await sleep(1000);
try {
const datasets = await getDatasets(GZFSDatasetTypeSchema.enum.ALL);
const datasets = await Promise.all([
getDatasets(GZFSDatasetTypeSchema.enum.FILESYSTEM),
getDatasets(GZFSDatasetTypeSchema.enum.VOLUME)
]).then(([filesystems, volumes]) => [...filesystems, ...volumes]);
const match = datasets.find((d) => {
if (d.name !== dataset) return false;
const enc = d.properties?.encryption?.value;
console.log(d.properties);
const enc = d.properties?.encryption || '';
return enc && enc !== 'off' && enc !== '-' && enc !== 'none';
});
srcEncrypted = !!match;
@@ -460,19 +468,42 @@
}
});
watch([() => open, () => form.mode, () => form.sourceDataset, () => form.selectedJailId, () => form.selectedVmId], ([isOpen]) => {
if (!isOpen) return;
let dataset = '';
if (form.mode === 'dataset') {
dataset = form.sourceDataset;
} else if (form.mode === 'jail' && selectedJail) {
const base = selectedJail.storages?.find((s) => s.isBase);
if (base) dataset = `${base.pool}/sylve/jails/${selectedJail.ctId}`;
} else if (form.mode === 'vm' && selectedVM) {
dataset = vmBaseDataset(selectedVM) || '';
watch(
[
() => open,
() => form.mode,
() => form.sourceDataset,
() => form.selectedJailId,
() => form.selectedVmId
],
([isOpen]) => {
if (!isOpen) return;
let dataset = '';
if (form.mode === 'dataset') {
dataset = form.sourceDataset;
} else if (form.mode === 'jail' && selectedJail) {
const base = selectedJail.storages?.find((s) => s.isBase);
if (base) dataset = `${base.pool}/sylve/jails/${selectedJail.ctId}`;
} else if (form.mode === 'vm' && selectedVM) {
dataset = vmBaseDataset(selectedVM) || '';
}
void checkSourceEncryption(dataset);
}
void checkSourceEncryption(dataset);
});
);
let disableStopBeforeBackup = $state(false);
watch(
() => form.mode,
(mode) => {
if (mode === 'dataset') {
form.stopBeforeBackup = false;
disableStopBeforeBackup = true;
} else {
disableStopBeforeBackup = false;
}
}
);
</script>
<Dialog.Root bind:open>
@@ -601,6 +632,10 @@
label="Stop before backup"
bind:checked={form.stopBeforeBackup}
classes="flex items-center gap-2"
disabled={disableStopBeforeBackup}
title={form.mode === 'dataset'
? 'This option is only applicable for jail and VM backups'
: ''}
/>
</div>
@@ -637,19 +672,6 @@
will be backed up
</li>
{/if}
<li>
Encryption:
<code class="rounded bg-background px-1">
{#if srcEncryptionChecking}
...
{:else if srcEncrypted}
<span class="icon-[material-symbols--lock] h-3.5 w-3.5 inline-block align-text-bottom"></span>
Enabled (keys auto-managed)
{:else}
None
{/if}
</code>
</li>
<li>
Schedule: <code class="rounded bg-background px-1"
>{cronToHuman(form.cronExpr) || '(not set)'}</code
@@ -669,12 +691,43 @@
>{form.pruneTarget ? 'Enabled' : 'Disabled'}</code
>
</li>
<li>
Stop before backup:
<code class="rounded bg-background px-1"
>{form.stopBeforeBackup ? 'Enabled' : 'Disabled'}</code
>
</li>
{#if !disableStopBeforeBackup}
<li>
Stop before backup:
<code class="rounded bg-background px-1"
>{form.stopBeforeBackup ? 'Enabled' : 'Disabled'}</code
>
</li>
{/if}
{#if form.mode !== 'jail' && form.mode !== 'vm'}
{#if srcEncryptionChecking}
<li>
<span
class="icon-[mdi--loading] h-3.5 w-3.5 animate-spin inline-block align-text-bottom mb-0.5"
></span>
</li>
{:else if form.sourceDataset === ''}
<li>
<span class="icon-[mdi--help] h-3.5 w-3.5 inline-block align-text-bottom mb-0.5"
></span>
<span>Select a dataset to view encryption information</span>
</li>
{:else if srcEncrypted}
<li>
<span class="icon-[mdi--lock] h-3.5 w-3.5 inline-block align-text-bottom mb-0.5"
></span>
<span>Source is encrypted</span>
</li>
{:else}
<li>
<span class="icon-[mdi--unlocked] h-3.5 w-3.5 inline-block align-text-bottom mb-0.5"
></span>
<span>Source is not encrypted</span>
</li>
{/if}
{/if}
</ul>
</div>
</div>
+34
View File
@@ -13920,3 +13920,37 @@ msgstr ""
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr ""
#~ msgid "Encryption: <0/>"
#~ msgstr ""
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr ""
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr ""
#~ msgid "Checking source encryption..."
#~ msgstr ""
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr ""
#~ msgid "No encryption information available"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr ""
+34
View File
@@ -13898,3 +13898,37 @@ msgstr ""
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr ""
#~ msgid "Encryption: <0/>"
#~ msgstr ""
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr ""
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr ""
#~ msgid "Checking source encryption..."
#~ msgstr ""
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr ""
#~ msgid "No encryption information available"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr ""
+34
View File
@@ -13921,3 +13921,37 @@ msgstr "VM Template - Create"
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr "Jail Template - Create"
#~ msgid "Encryption: <0/>"
#~ msgstr "Encryption: <0/>"
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr "Enabled (keys auto-managed)"
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgid "Checking source encryption..."
#~ msgstr "Checking source encryption..."
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr "Source is encrypted (keys auto-managed)"
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr "Source is not encrypted"
#~ msgid "No encryption information available"
#~ msgstr "No encryption information available"
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr "Select a dataset to view encryption information"
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr "This option is only applicable for jail and VM backups"
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr "Source is encrypted"
+34
View File
@@ -13898,3 +13898,37 @@ msgstr ""
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr ""
#~ msgid "Encryption: <0/>"
#~ msgstr ""
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr ""
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr ""
#~ msgid "Checking source encryption..."
#~ msgstr ""
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr ""
#~ msgid "No encryption information available"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr ""
+34
View File
@@ -13898,3 +13898,37 @@ msgstr ""
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr ""
#~ msgid "Encryption: <0/>"
#~ msgstr ""
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr ""
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr ""
#~ msgid "Checking source encryption..."
#~ msgstr ""
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr ""
#~ msgid "No encryption information available"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr ""
+34
View File
@@ -13895,3 +13895,37 @@ msgstr ""
#: src/lib/components/skeleton/BottomPanel.svelte
msgid "Jail Template - Create"
msgstr ""
#~ msgid "Encryption: <0/>"
#~ msgstr ""
#~ msgid "Enabled (keys auto-managed)"
#~ msgstr ""
#~ msgid "Encryption: <0>Enabled (keys auto-managed)</0>"
#~ msgstr ""
#~ msgid "Checking source encryption..."
#~ msgstr ""
#~ msgid "Source is encrypted (keys auto-managed)"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is not encrypted"
msgstr ""
#~ msgid "No encryption information available"
#~ msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Select a dataset to view encryption information"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "This option is only applicable for jail and VM backups"
msgstr ""
#: src/lib/components/custom/DataCenter/Backups/Jobs/Form.svelte
msgid "Source is encrypted"
msgstr ""