host-backup(pbs): fix scheduled-job encryption + surface real key import error + pass --keyfile on downloads

Three bugs against the PBS encryption flow:

1. Create-scheduled-job with encryption failed with "Recovery setup
   failed: no PBS keyfile present" whenever the operator picked
   "Generate a new keyfile" but had no keyfile installed yet. The
   frontend called /pbs-recovery/setup before creating the job, but
   the keyfile was only materialised later during job creation. The
   endpoint now generates the keyfile atomically if missing before
   building the escrow blob — same prompt-first order the CLI wizard
   applies. Existing keyfiles are still trusted and never rotated.

2. Importing a valid PBS keyfile via the Web dialog returned a
   generic "did not recognise this file as a valid PBS keyfile" that
   hid the real reason (kdf mismatch, missing passphrase, corrupt
   JSON, ...). The endpoint now attaches the stderr of
   `proxmox-backup-client key info` as `tool_output` and the frontend
   renders it verbatim inside the red banner. Also strips a leading
   UTF-8 BOM before validating so an editor-inserted BOM stops being
   silently classified as "invalid keyfile".

3. Downloading an encrypted PBS snapshot failed with "missing key —
   manifest was created with key XX:XX:..." even when the correct
   keyfile was installed at /usr/local/share/proxmenux/pbs-key.conf,
   because the restore worker invoked `proxmox-backup-client restore`
   without `--keyfile`. The flag is now passed whenever a local
   keyfile exists (PBS ignores it for unencrypted archives). On a
   fingerprint mismatch the error now appends the installed key's
   fingerprint so it can be compared side-by-side with the manifest's
   expected value — same fingerprint also exposed via
   /pbs-recovery/status for the UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-07-05 23:14:52 +02:00
co-authored by Claude Opus 4.7
parent 4b022d05f2
commit 26b47e63d9
2 changed files with 118 additions and 22 deletions
+29 -13
View File
@@ -452,7 +452,7 @@ export function HostBackup() {
) : jobsResp.jobs.filter((j) => !j.manual).length === 0 ? null : (
<div className="space-y-2">
{actionError && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{actionError}
</div>
)}
@@ -1419,7 +1419,7 @@ function InspectModal({
)}
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -2389,7 +2389,15 @@ function CreateJobDialog({
})
mutatePbsRecovery()
} catch (e) {
setError(`Keyfile import failed: ${e instanceof Error ? e.message : String(e)}`)
// The import endpoint returns `{error, tool_output, tool_exit_code}`
// on validation failure. Surface the tool output verbatim so the
// operator can tell a real "not a keyfile" from a "keyfile needs
// a passphrase / uses unsupported KDF" and act on it.
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
const detail = err.body?.tool_output
? `${err.message}\n\nproxmox-backup-client output:\n${err.body.tool_output}`
: err.message
setError(`Keyfile import failed: ${detail || String(e)}`)
setPbsImportBusy(false)
setSubmitting(false)
return
@@ -3390,7 +3398,7 @@ function CreateJobDialog({
})()}
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -3653,7 +3661,15 @@ function ManualBackupDialog({
})
mutatePbsRecovery()
} catch (e) {
setError(`Keyfile import failed: ${e instanceof Error ? e.message : String(e)}`)
// The import endpoint returns `{error, tool_output, tool_exit_code}`
// on validation failure. Surface the tool output verbatim so the
// operator can tell a real "not a keyfile" from a "keyfile needs
// a passphrase / uses unsupported KDF" and act on it.
const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
const detail = err.body?.tool_output
? `${err.message}\n\nproxmox-backup-client output:\n${err.body.tool_output}`
: err.message
setError(`Keyfile import failed: ${detail || String(e)}`)
setPbsImportBusy(false)
setSubmitting(false)
return
@@ -4188,7 +4204,7 @@ function ManualBackupDialog({
</div>
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -4527,7 +4543,7 @@ function DestinationsSection({
</Button>
</div>
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -5539,7 +5555,7 @@ function AddDestinationDialog({
</div>
)}
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -5905,7 +5921,7 @@ function ExtraPathsSection() {
</p>
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -6090,7 +6106,7 @@ function UsbDrivesSection() {
</p>
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}
@@ -6441,7 +6457,7 @@ function JobDetailModal({
</DialogHeader>
{actionError && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{actionError}
</div>
)}
@@ -6818,7 +6834,7 @@ function ManualJobWatchModal({
</DialogHeader>
{actionError && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{actionError}
</div>
)}
@@ -7065,7 +7081,7 @@ function PbsKeyfileRecoveryDialog({
</div>
{error && (
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
{error}
</div>
)}