mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
update 1.2.2.3 beta
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
||||
1678cb55256ffcfb62f451b631aa174c014b36319c857b7d066cde8d0431c7f1 ProxMenux-1.2.2.3-beta.AppImage
|
||||
7eb39d1bb3aed0750f32708fab66bf5c2c95a47da5722b9bd351b34c7c31b3ee ProxMenux-1.2.2.3-beta.AppImage
|
||||
|
||||
@@ -2046,6 +2046,7 @@ function CreateJobDialog({
|
||||
// File picked in the "existing" mode. Kept in a ref-like state so a
|
||||
// stale reference doesn't linger across modal opens.
|
||||
const [pbsImportFile, setPbsImportFile] = useState<File | null>(null)
|
||||
const [pbsImportKeyfilePass, setPbsImportKeyfilePass] = useState<string>("")
|
||||
const [pbsImportBusy, setPbsImportBusy] = useState<boolean>(false)
|
||||
// Legacy alias for the many recovery / gating checks below — a truthy
|
||||
// value means "the operator wants an encrypted backup" regardless of
|
||||
@@ -2066,7 +2067,7 @@ function CreateJobDialog({
|
||||
// present, the passphrase input becomes optional ("leave blank to
|
||||
// keep saved"). Refreshed after a successful setup call.
|
||||
const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
|
||||
has_keyfile: boolean; has_recovery: boolean
|
||||
has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean
|
||||
}>(
|
||||
open && backend === "pbs" ? "/api/host-backups/pbs-recovery/status" : null,
|
||||
fetcher,
|
||||
@@ -2246,6 +2247,7 @@ function CreateJobDialog({
|
||||
setPbsBackupId("")
|
||||
setPbsEncryptMode("none")
|
||||
setPbsImportFile(null)
|
||||
setPbsImportKeyfilePass("")
|
||||
setPbsImportBusy(false)
|
||||
setPbsRecoveryPass("")
|
||||
setPbsRecoveryPass2("")
|
||||
@@ -2385,7 +2387,10 @@ function CreateJobDialog({
|
||||
await fetchApi("/api/host-backups/pbs-encryption/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content_b64 }),
|
||||
body: JSON.stringify({
|
||||
content_b64,
|
||||
keyfile_passphrase: pbsImportKeyfilePass,
|
||||
}),
|
||||
})
|
||||
mutatePbsRecovery()
|
||||
} catch (e) {
|
||||
@@ -3093,21 +3098,40 @@ function CreateJobDialog({
|
||||
</div>
|
||||
</label>
|
||||
{pbsEncryptMode === "existing" && (
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<Label htmlFor="pbsImportFile" className="text-[11px] font-medium">
|
||||
Keyfile to import
|
||||
</Label>
|
||||
<Input
|
||||
id="pbsImportFile"
|
||||
type="file"
|
||||
accept=".conf,.key,application/json,text/plain"
|
||||
onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
|
||||
disabled={pbsImportBusy}
|
||||
className="h-8 text-[11px]"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Validated with <code className="font-mono">proxmox-backup-client key info</code> before install. On validation failure the job is not created and no keyfile is written.
|
||||
</p>
|
||||
<div className="space-y-2 pt-1">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="pbsImportFile" className="text-[11px] font-medium">
|
||||
Keyfile to import
|
||||
</Label>
|
||||
<Input
|
||||
id="pbsImportFile"
|
||||
type="file"
|
||||
accept=".conf,.key,application/json,text/plain"
|
||||
onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
|
||||
disabled={pbsImportBusy}
|
||||
className="h-8 text-[11px]"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
ProxMenux does not validate the file — any keyfile you accept as valid on your PBS is accepted here. If it later fails at backup time, the tool's error tells you why.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="pbsImportKeyfilePass" className="text-[11px] font-medium">
|
||||
Keyfile passphrase (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="pbsImportKeyfilePass"
|
||||
type="password"
|
||||
value={pbsImportKeyfilePass}
|
||||
onChange={(e) => setPbsImportKeyfilePass(e.target.value)}
|
||||
disabled={pbsImportBusy}
|
||||
placeholder="Leave blank if the keyfile has no passphrase"
|
||||
className="h-8 text-[11px] font-mono"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
If the keyfile was generated with <code className="font-mono">proxmox-backup-client key create --kdf scrypt</code> (the tool's default) it needs the original passphrase to be unlocked at backup time. Leave blank for <code className="font-mono">--kdf none</code> keyfiles. Stored at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.pass</code> (chmod 600).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3512,6 +3536,7 @@ function ManualBackupDialog({
|
||||
// block — the three modes and the import flow are identical here.
|
||||
const [pbsEncryptMode, setPbsEncryptMode] = useState<"none" | "new" | "existing">("none")
|
||||
const [pbsImportFile, setPbsImportFile] = useState<File | null>(null)
|
||||
const [pbsImportKeyfilePass, setPbsImportKeyfilePass] = useState<string>("")
|
||||
const [pbsImportBusy, setPbsImportBusy] = useState<boolean>(false)
|
||||
const pbsEncrypt = pbsEncryptMode !== "none"
|
||||
const [pbsRecoveryPass, setPbsRecoveryPass] = useState<string>("")
|
||||
@@ -3525,7 +3550,7 @@ function ManualBackupDialog({
|
||||
// present, the passphrase input becomes optional ("leave blank to
|
||||
// keep saved"). Refreshed after a successful setup call.
|
||||
const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
|
||||
has_keyfile: boolean; has_recovery: boolean
|
||||
has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean
|
||||
}>(
|
||||
open && backend === "pbs" ? "/api/host-backups/pbs-recovery/status" : null,
|
||||
fetcher,
|
||||
@@ -3578,6 +3603,7 @@ function ManualBackupDialog({
|
||||
setPbsBackupId("")
|
||||
setPbsEncryptMode("none")
|
||||
setPbsImportFile(null)
|
||||
setPbsImportKeyfilePass("")
|
||||
setPbsImportBusy(false)
|
||||
setPbsRecoveryPass("")
|
||||
setPbsRecoveryPass2("")
|
||||
@@ -3657,7 +3683,10 @@ function ManualBackupDialog({
|
||||
await fetchApi("/api/host-backups/pbs-encryption/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content_b64 }),
|
||||
body: JSON.stringify({
|
||||
content_b64,
|
||||
keyfile_passphrase: pbsImportKeyfilePass,
|
||||
}),
|
||||
})
|
||||
mutatePbsRecovery()
|
||||
} catch (e) {
|
||||
@@ -3977,21 +4006,40 @@ function ManualBackupDialog({
|
||||
</div>
|
||||
</label>
|
||||
{pbsEncryptMode === "existing" && (
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<Label htmlFor="manualPbsImportFile" className="text-[11px] font-medium">
|
||||
Keyfile to import
|
||||
</Label>
|
||||
<Input
|
||||
id="manualPbsImportFile"
|
||||
type="file"
|
||||
accept=".conf,.key,application/json,text/plain"
|
||||
onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
|
||||
disabled={pbsImportBusy}
|
||||
className="h-8 text-[11px]"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Validated with <code className="font-mono">proxmox-backup-client key info</code> before install.
|
||||
</p>
|
||||
<div className="space-y-2 pt-1">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="manualPbsImportFile" className="text-[11px] font-medium">
|
||||
Keyfile to import
|
||||
</Label>
|
||||
<Input
|
||||
id="manualPbsImportFile"
|
||||
type="file"
|
||||
accept=".conf,.key,application/json,text/plain"
|
||||
onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
|
||||
disabled={pbsImportBusy}
|
||||
className="h-8 text-[11px]"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
ProxMenux does not validate the file — any keyfile accepted by your PBS is accepted here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="manualPbsImportKeyfilePass" className="text-[11px] font-medium">
|
||||
Keyfile passphrase (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="manualPbsImportKeyfilePass"
|
||||
type="password"
|
||||
value={pbsImportKeyfilePass}
|
||||
onChange={(e) => setPbsImportKeyfilePass(e.target.value)}
|
||||
disabled={pbsImportBusy}
|
||||
placeholder="Leave blank if the keyfile has no passphrase"
|
||||
className="h-8 text-[11px] font-mono"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Only needed for keyfiles created with <code className="font-mono">--kdf scrypt</code> (the default of <code className="font-mono">proxmox-backup-client key create</code>). Stored at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.pass</code> (chmod 600) and reused by every encrypted job on this host.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -4354,7 +4402,7 @@ function DestinationsSection({
|
||||
// exists. Drives the "Recover keyfile" banner.
|
||||
const hasPbs = (destinations?.pbs?.length ?? 0) > 0
|
||||
const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
|
||||
has_keyfile: boolean; has_recovery: boolean
|
||||
has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean
|
||||
}>(hasPbs ? "/api/host-backups/pbs-recovery/status" : null, fetcher)
|
||||
const { data: pbsRecoveryAvailable } = useSWR<{
|
||||
snapshots: Array<{ repo_name: string; repo_repository: string; backup_id: string; source_host: string; backup_time: number; snapshot: string }>
|
||||
|
||||
@@ -13738,6 +13738,14 @@ _PBS_KEYFILE_PATH = f'{_BACKUP_STATE_DIR}/pbs-key.conf'
|
||||
# shell's hb_pbs_setup_recovery flow byte-for-byte (AES-256-CBC +
|
||||
# PBKDF2 600k iterations).
|
||||
_PBS_RECOVERY_ENC_PATH = f'{_BACKUP_STATE_DIR}/pbs-key.recovery.enc'
|
||||
# Passphrase used to unlock the keyfile itself when it was created with
|
||||
# `--kdf scrypt`. Written by the "Import existing keyfile" flow when
|
||||
# the operator provides one; empty file (or missing) means the keyfile
|
||||
# is kdf=none and no passphrase is needed. Read at backup time by the
|
||||
# runner and exported as PBS_ENCRYPTION_PASSWORD so
|
||||
# proxmox-backup-client can decrypt the key without prompting.
|
||||
# Same trust boundary as the keyfile itself: chmod 600, next to it.
|
||||
_PBS_KEYFILE_PASS_PATH = f'{_BACKUP_STATE_DIR}/pbs-key.pass'
|
||||
|
||||
|
||||
def _pbs_keyfile_get_or_create(create_if_missing: bool = True) -> str:
|
||||
@@ -13843,6 +13851,13 @@ def api_pbs_recovery_status():
|
||||
return jsonify({
|
||||
'has_keyfile': os.path.isfile(_PBS_KEYFILE_PATH),
|
||||
'has_recovery': os.path.isfile(_PBS_RECOVERY_ENC_PATH),
|
||||
# True only when a non-empty passphrase file exists next to the
|
||||
# keyfile. Lets the UI skip re-prompting the operator for the
|
||||
# scrypt-unlock passphrase during subsequent job creations.
|
||||
'has_keyfile_passphrase': (
|
||||
os.path.isfile(_PBS_KEYFILE_PASS_PATH)
|
||||
and os.path.getsize(_PBS_KEYFILE_PASS_PATH) > 0
|
||||
),
|
||||
'keyfile_path': _PBS_KEYFILE_PATH,
|
||||
'recovery_path': _PBS_RECOVERY_ENC_PATH,
|
||||
# Fingerprint of the installed keyfile — empty when no keyfile
|
||||
@@ -14041,12 +14056,18 @@ def api_pbs_encryption_import():
|
||||
|
||||
# Strip a leading UTF-8 BOM (b'\xef\xbb\xbf') if the operator's
|
||||
# editor added one — the byte order mark makes the file no longer
|
||||
# a valid JSON document and `key info` refuses it with a parse
|
||||
# error that reads like a generic "not a keyfile", which sends
|
||||
# the operator down the wrong debugging path.
|
||||
# a valid JSON document and downstream tools refuse it with a
|
||||
# cryptic parse error.
|
||||
if content.startswith(b'\xef\xbb\xbf'):
|
||||
content = content[3:]
|
||||
|
||||
# Optional passphrase for a scrypt-encoded keyfile. Blank means the
|
||||
# keyfile is kdf=none. We do NOT verify that the passphrase actually
|
||||
# unlocks the keyfile — that would require the operator to be
|
||||
# present and the flow to be interactive. If it's wrong, backups
|
||||
# will fail loudly with proxmox-backup-client's own error message.
|
||||
keyfile_pass = payload.get('keyfile_passphrase') or ''
|
||||
|
||||
import tempfile
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix='pbs-key.import.', dir=_BACKUP_STATE_DIR)
|
||||
try:
|
||||
@@ -14057,37 +14078,26 @@ def api_pbs_encryption_import():
|
||||
except OSError as e:
|
||||
return jsonify({'error': f'cannot write tmp keyfile: {e}'}), 500
|
||||
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['proxmox-backup-client', 'key', 'info', '--output-format', 'json', tmp_path],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as e:
|
||||
return jsonify({'error': f'validation failed: {type(e).__name__}'}), 500
|
||||
|
||||
if r.returncode != 0:
|
||||
# Surface the real tool output so the operator can tell a
|
||||
# bad-file error from a needs-passphrase error (keyfiles
|
||||
# generated with `--kdf scrypt` need to be decrypted with
|
||||
# their original passphrase before they can be reused as an
|
||||
# import — ProxMenux only supports kdf=none for the
|
||||
# canonical shared keyfile).
|
||||
tool_output = (r.stderr or r.stdout or '').strip()
|
||||
return jsonify({
|
||||
'error': 'proxmox-backup-client did not recognise this file as a valid PBS keyfile',
|
||||
'tool_output': tool_output[:800] if tool_output else '(no output from proxmox-backup-client key info)',
|
||||
'tool_exit_code': r.returncode,
|
||||
}), 400
|
||||
# No content validation. The operator brings their own key —
|
||||
# any KDF, any format, any origin. If it later doesn't work
|
||||
# with proxmox-backup-client the failure will surface at
|
||||
# backup time with a clear error. This is a deliberate
|
||||
# trust-the-operator design: keeping `key info` validation
|
||||
# here rejected legitimate scrypt-encoded keyfiles that could
|
||||
# only be inspected with their original passphrase, which is
|
||||
# exactly the case ProxMenux can't provide in a non-interactive
|
||||
# flow.
|
||||
|
||||
# The old recovery blob was encrypted with the old key — a new
|
||||
# blob has to be built by re-running /pbs-recovery/setup once
|
||||
# the operator confirms this import.
|
||||
try:
|
||||
os.remove(_PBS_RECOVERY_ENC_PATH)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
for stale in (_PBS_RECOVERY_ENC_PATH, _PBS_KEYFILE_PASS_PATH):
|
||||
try:
|
||||
os.remove(stale)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.replace(tmp_path, _PBS_KEYFILE_PATH)
|
||||
@@ -14095,9 +14105,22 @@ def api_pbs_encryption_import():
|
||||
except OSError as e:
|
||||
return jsonify({'error': f'cannot promote keyfile: {e}'}), 500
|
||||
|
||||
# Persist the keyfile passphrase alongside the key, chmod 600.
|
||||
# Same trust boundary as the keyfile itself: root-only. Empty
|
||||
# value → don't write anything (blank passphrase == kdf=none).
|
||||
if keyfile_pass:
|
||||
try:
|
||||
fd = os.open(_PBS_KEYFILE_PASS_PATH,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
f.write(keyfile_pass)
|
||||
except OSError as e:
|
||||
return jsonify({'error': f'cannot store keyfile passphrase: {e}'}), 500
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'keyfile_path': _PBS_KEYFILE_PATH,
|
||||
'keyfile_pass_stored': bool(keyfile_pass),
|
||||
'recovery_reset': True,
|
||||
})
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user