mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
update 1.2.2.2 beta
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
||||
33e64043c50e9d90c1297019219c08d5196ab8181f0409e77f1dce9c42b0e73e ProxMenux-1.2.2.2-beta.AppImage
|
||||
08b669193097449f8330ad430066574bf0e99f477aea97172e6b845ad4ee648c ProxMenux-1.2.2.2-beta.AppImage
|
||||
|
||||
@@ -810,17 +810,9 @@ entities:
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
|
||||
This project is licensed under the **GNU General Public License, version 3 (GPL-3.0)**.
|
||||
|
||||
You are free to:
|
||||
- Share — copy and redistribute the material in any medium or format
|
||||
- Adapt — remix, transform, and build upon the material
|
||||
|
||||
Under the following terms:
|
||||
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
|
||||
- NonCommercial — You may not use the material for commercial purposes
|
||||
|
||||
For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
You are free to use, study, share and modify the software under the terms of the licence. Any distributed derivative work must be licensed under the same terms and include the full source code — see the [full licence text](https://www.gnu.org/licenses/gpl-3.0.html) or the [`LICENSE`](https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) file at the repository root.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1972,7 +1972,11 @@ interface JobDetail {
|
||||
pbs_backup_id: string | null
|
||||
pbs_fingerprint: string | null
|
||||
has_pbs_password: boolean
|
||||
// Legacy — kept for backend responses that only track the on/off
|
||||
// toggle. Newer builds also send pbs_encrypt_mode ("none" | "new"
|
||||
// | "existing"); loadFromJobDetail prefers that when present.
|
||||
pbs_encrypt: boolean
|
||||
pbs_encrypt_mode?: "none" | "new" | "existing"
|
||||
local_dest_dir: string | null
|
||||
local_archive_ext: string | null
|
||||
borg_repo: string | null
|
||||
@@ -2025,10 +2029,21 @@ function CreateJobDialog({
|
||||
// Backend-specific fields
|
||||
const [pbsRepository, setPbsRepository] = useState<string>("")
|
||||
const [pbsBackupId, setPbsBackupId] = useState<string>("")
|
||||
// Client-side PBS encryption — sent as `pbs_encrypt` in the payload.
|
||||
// Backend resolves the shared keyfile + injects PBS_KEYFILE into
|
||||
// the job .env so the runner adds `--keyfile` to the backup call.
|
||||
const [pbsEncrypt, setPbsEncrypt] = useState<boolean>(false)
|
||||
// Client-side PBS encryption mode. Three modes match the shell wizard:
|
||||
// - "none" — plain backup, no --keyfile
|
||||
// - "new" — backend generates a fresh per-host keyfile (default)
|
||||
// - "existing" — operator uploads their own keyfile (shared across
|
||||
// hosts). The upload happens via a dedicated endpoint
|
||||
// BEFORE the job create call — see handleCreate below.
|
||||
const [pbsEncryptMode, setPbsEncryptMode] = useState<"none" | "new" | "existing">("none")
|
||||
// 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 [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
|
||||
// whether that keyfile came from a fresh generate or from an import.
|
||||
const pbsEncrypt = pbsEncryptMode !== "none"
|
||||
// Optional recovery passphrase. When set, the backend encrypts the
|
||||
// keyfile with openssl and the runner uploads that blob to PBS with
|
||||
// every backup so the keyfile can be rebuilt on a fresh host with
|
||||
@@ -2123,7 +2138,15 @@ function CreateJobDialog({
|
||||
if (r.keep_yearly !== undefined) setKeepYearly(String(r.keep_yearly))
|
||||
if (jobDetail.pbs_repository) setPbsRepository(jobDetail.pbs_repository)
|
||||
if (jobDetail.pbs_backup_id) setPbsBackupId(jobDetail.pbs_backup_id)
|
||||
setPbsEncrypt(!!jobDetail.pbs_encrypt)
|
||||
// Prefer the new pbs_encrypt_mode string when the backend sends it;
|
||||
// fall back to the legacy bool for older `.env` layouts that only
|
||||
// remember the on/off toggle.
|
||||
const jd = jobDetail as unknown as { pbs_encrypt_mode?: string; pbs_encrypt?: boolean }
|
||||
if (jd.pbs_encrypt_mode === "new" || jd.pbs_encrypt_mode === "existing" || jd.pbs_encrypt_mode === "none") {
|
||||
setPbsEncryptMode(jd.pbs_encrypt_mode)
|
||||
} else {
|
||||
setPbsEncryptMode(jd.pbs_encrypt ? "new" : "none")
|
||||
}
|
||||
if (jobDetail.local_dest_dir) setLocalDestDir(jobDetail.local_dest_dir)
|
||||
if (jobDetail.borg_repo) setBorgRepoSelected(jobDetail.borg_repo)
|
||||
if (jobDetail.borg_encrypt_mode) {
|
||||
@@ -2214,7 +2237,9 @@ function CreateJobDialog({
|
||||
setKeepYearly("0")
|
||||
setPbsRepository("")
|
||||
setPbsBackupId("")
|
||||
setPbsEncrypt(false)
|
||||
setPbsEncryptMode("none")
|
||||
setPbsImportFile(null)
|
||||
setPbsImportBusy(false)
|
||||
setPbsRecoveryPass("")
|
||||
setPbsRecoveryPass2("")
|
||||
setPbsRecoveryChange(false)
|
||||
@@ -2322,6 +2347,39 @@ function CreateJobDialog({
|
||||
if (mode === "attach" && !selectedPveJob) return
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
// "Import existing" mode: push the user-supplied keyfile to the
|
||||
// canonical shared path BEFORE anything else. This mirrors the
|
||||
// shell wizard where hb_pbs_import_keyfile runs before recovery
|
||||
// setup and before the job .env is written. If the upload fails
|
||||
// we bail loudly instead of silently downgrading to "generate new".
|
||||
if (backend === "pbs" && pbsEncryptMode === "existing") {
|
||||
if (!pbsImportFile) {
|
||||
setError("Pick a keyfile to import.")
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
setPbsImportBusy(true)
|
||||
try {
|
||||
const buf = new Uint8Array(await pbsImportFile.arrayBuffer())
|
||||
// btoa on binary requires a String.fromCharCode round-trip.
|
||||
// The keyfile is small (~200 bytes), so no perf concern.
|
||||
let bin = ""
|
||||
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
|
||||
const content_b64 = btoa(bin)
|
||||
await fetchApi("/api/host-backups/pbs-encryption/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content_b64 }),
|
||||
})
|
||||
mutatePbsRecovery()
|
||||
} catch (e) {
|
||||
setError(`Keyfile import failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||
setPbsImportBusy(false)
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
setPbsImportBusy(false)
|
||||
}
|
||||
// Configure the PBS recovery escrow BEFORE creating the job so the
|
||||
// very first backup the runner triggers can already upload the
|
||||
// blob. Only fires when the operator typed a passphrase pair.
|
||||
@@ -2374,7 +2432,7 @@ function CreateJobDialog({
|
||||
body.pbs_password = ""
|
||||
if (pbsBackupId) body.pbs_backup_id = pbsBackupId
|
||||
if (selectedPbs?.fingerprint) body.pbs_fingerprint = selectedPbs.fingerprint
|
||||
body.pbs_encrypt = pbsEncrypt
|
||||
body.pbs_encrypt_mode = pbsEncryptMode
|
||||
} else if (backend === "local") {
|
||||
if (localDestDir.trim()) body.local_dest_dir = localDestDir.trim()
|
||||
} else if (backend === "borg") {
|
||||
@@ -2938,23 +2996,50 @@ function CreateJobDialog({
|
||||
</p>
|
||||
</div>
|
||||
{/* PBS client-side encryption — same flow as the
|
||||
shell wizard. A shared keyfile under /usr/local/
|
||||
share/proxmenux/pbs-key.conf is generated on
|
||||
first use and reused by every encrypted job. */}
|
||||
shell wizard. The keyfile can be freshly generated
|
||||
per host or imported from an existing one the
|
||||
operator uses across every host. */}
|
||||
<div className="pt-2 border-t border-border space-y-3">
|
||||
<Label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={pbsEncrypt}
|
||||
onCheckedChange={(v) => setPbsEncrypt(!!v)}
|
||||
/>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="inline-flex items-center gap-1.5">
|
||||
<Lock className="h-3.5 w-3.5 text-emerald-400" />
|
||||
Encrypt backups (client-side keyfile)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground pl-7">
|
||||
Adds <code className="font-mono">--keyfile</code> to <code className="font-mono">proxmox-backup-client backup</code>. Encryption happens before upload so chunks land on the PBS server already ciphered. The keyfile lives at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> (chmod 0600) and is shared across every encrypted PBS job on this host.
|
||||
</p>
|
||||
</Label>
|
||||
<Select
|
||||
value={pbsEncryptMode}
|
||||
onValueChange={(v) => setPbsEncryptMode(v as "none" | "new" | "existing")}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No encryption</SelectItem>
|
||||
<SelectItem value="new">Generate a new keyfile (per host — safest isolation)</SelectItem>
|
||||
<SelectItem value="existing">Import an existing keyfile (shared across hosts)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Adds <code className="font-mono">--keyfile</code> to <code className="font-mono">proxmox-backup-client backup</code>. Encryption happens before upload so chunks land on PBS already ciphered. The keyfile is installed at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> (chmod 0600) and reused by every encrypted PBS job on this host.
|
||||
</p>
|
||||
</div>
|
||||
{pbsEncryptMode === "existing" && (
|
||||
<div className="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3">
|
||||
<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">
|
||||
The file is validated with <code className="font-mono">proxmox-backup-client key info</code> before being installed. If validation fails the job is not created and the existing keyfile (if any) stays in place.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{pbsEncrypt && (
|
||||
<div className="pl-7 space-y-2 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
|
||||
<div className="text-[11px] font-medium text-foreground flex items-center gap-1.5">
|
||||
@@ -3350,7 +3435,12 @@ function ManualBackupDialog({
|
||||
|
||||
const [pbsRepository, setPbsRepository] = useState<string>("")
|
||||
const [pbsBackupId, setPbsBackupId] = useState<string>("")
|
||||
const [pbsEncrypt, setPbsEncrypt] = useState<boolean>(false)
|
||||
// Encryption mode. See CreateJobDialog above for the full comment
|
||||
// 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 [pbsImportBusy, setPbsImportBusy] = useState<boolean>(false)
|
||||
const pbsEncrypt = pbsEncryptMode !== "none"
|
||||
const [pbsRecoveryPass, setPbsRecoveryPass] = useState<string>("")
|
||||
const [pbsRecoveryPass2, setPbsRecoveryPass2] = useState<string>("")
|
||||
// Operator opted to replace an already-configured recovery escrow.
|
||||
@@ -3413,7 +3503,9 @@ function ManualBackupDialog({
|
||||
setCustomPaths(new Set())
|
||||
setPbsRepository("")
|
||||
setPbsBackupId("")
|
||||
setPbsEncrypt(false)
|
||||
setPbsEncryptMode("none")
|
||||
setPbsImportFile(null)
|
||||
setPbsImportBusy(false)
|
||||
setPbsRecoveryPass("")
|
||||
setPbsRecoveryPass2("")
|
||||
setPbsRecoveryChange(false)
|
||||
@@ -3470,6 +3562,35 @@ function ManualBackupDialog({
|
||||
if (!canSubmit) return
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
// "Import existing" mode: same shape as CreateJobDialog — push the
|
||||
// keyfile to the canonical path before anything else, bail loudly
|
||||
// if validation fails so we don't silently fall back to "new".
|
||||
if (backend === "pbs" && pbsEncryptMode === "existing") {
|
||||
if (!pbsImportFile) {
|
||||
setError("Pick a keyfile to import.")
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
setPbsImportBusy(true)
|
||||
try {
|
||||
const buf = new Uint8Array(await pbsImportFile.arrayBuffer())
|
||||
let bin = ""
|
||||
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
|
||||
const content_b64 = btoa(bin)
|
||||
await fetchApi("/api/host-backups/pbs-encryption/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content_b64 }),
|
||||
})
|
||||
mutatePbsRecovery()
|
||||
} catch (e) {
|
||||
setError(`Keyfile import failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||
setPbsImportBusy(false)
|
||||
setSubmitting(false)
|
||||
return
|
||||
}
|
||||
setPbsImportBusy(false)
|
||||
}
|
||||
// Same pre-step as CreateJob: configure the PBS recovery escrow
|
||||
// before the run so the runner's post-backup upload of the blob
|
||||
// has something to push.
|
||||
@@ -3505,7 +3626,7 @@ function ManualBackupDialog({
|
||||
body.pbs_password = ""
|
||||
if (pbsBackupId) body.pbs_backup_id = pbsBackupId
|
||||
if (selectedPbs?.fingerprint) body.pbs_fingerprint = selectedPbs.fingerprint
|
||||
body.pbs_encrypt = pbsEncrypt
|
||||
body.pbs_encrypt_mode = pbsEncryptMode
|
||||
} else if (backend === "local") {
|
||||
if (localDestDir.trim()) body.local_dest_dir = localDestDir.trim()
|
||||
} else if (backend === "borg") {
|
||||
@@ -3706,19 +3827,46 @@ function ManualBackupDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-border space-y-3">
|
||||
<Label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={pbsEncrypt}
|
||||
onCheckedChange={(v) => setPbsEncrypt(!!v)}
|
||||
/>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<div className="space-y-2">
|
||||
<Label className="inline-flex items-center gap-1.5">
|
||||
<Lock className="h-3.5 w-3.5 text-emerald-400" />
|
||||
Encrypt this backup (client-side keyfile)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground pl-7">
|
||||
Uses the shared keyfile at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> (auto-generated on first use).
|
||||
</p>
|
||||
</Label>
|
||||
<Select
|
||||
value={pbsEncryptMode}
|
||||
onValueChange={(v) => setPbsEncryptMode(v as "none" | "new" | "existing")}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No encryption</SelectItem>
|
||||
<SelectItem value="new">Generate a new keyfile (per host — safest isolation)</SelectItem>
|
||||
<SelectItem value="existing">Import an existing keyfile (shared across hosts)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Uses the shared keyfile at <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code>.
|
||||
</p>
|
||||
</div>
|
||||
{pbsEncryptMode === "existing" && (
|
||||
<div className="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3">
|
||||
<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>
|
||||
)}
|
||||
{pbsEncrypt && (
|
||||
<div className="pl-7 space-y-2 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
|
||||
<div className="text-[11px] font-medium text-foreground flex items-center gap-1.5">
|
||||
|
||||
@@ -13803,6 +13803,99 @@ def api_pbs_recovery_status():
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/host-backups/pbs-encryption/import', methods=['POST'])
|
||||
@require_auth
|
||||
def api_pbs_encryption_import():
|
||||
"""Install a user-provided PBS keyfile at the canonical shared path.
|
||||
|
||||
Body: ``{"content_b64": "<base64 of the keyfile bytes>"}``
|
||||
|
||||
Use case: operators who generated their own PBS master key and want
|
||||
to share it across every host (single recovery blob covers all,
|
||||
cross-host restore is possible). The keyfile is written to a tmp
|
||||
path first and validated with ``proxmox-backup-client key info``;
|
||||
only if the tool accepts it as a real PBS keyfile does it get
|
||||
promoted to the canonical ``_PBS_KEYFILE_PATH``. Any prior
|
||||
keyfile + recovery blob at that location are replaced atomically —
|
||||
the old blob was encrypted for the old key and would no longer be
|
||||
decryptable anyway.
|
||||
"""
|
||||
payload = request.get_json(silent=True) or {}
|
||||
content_b64 = payload.get('content_b64') or ''
|
||||
if not content_b64:
|
||||
return jsonify({'error': 'content_b64 is required'}), 400
|
||||
|
||||
import base64
|
||||
try:
|
||||
content = base64.b64decode(content_b64, validate=True)
|
||||
except Exception:
|
||||
return jsonify({'error': 'content_b64 is not valid base64'}), 400
|
||||
|
||||
# PBS keyfiles are compact JSON blobs (~200 bytes). Cap at 8 KB so
|
||||
# a malformed upload can't fill the state dir with garbage.
|
||||
if not content or len(content) > 8192:
|
||||
return jsonify({'error': 'keyfile size out of expected range (1..8192 bytes)'}), 400
|
||||
|
||||
if not shutil.which('proxmox-backup-client'):
|
||||
return jsonify({'error': 'proxmox-backup-client is not installed on this host'}), 500
|
||||
|
||||
try:
|
||||
os.makedirs(_BACKUP_STATE_DIR, exist_ok=True)
|
||||
except OSError as e:
|
||||
return jsonify({'error': f'cannot create state directory: {e}'}), 500
|
||||
|
||||
import tempfile
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix='pbs-key.import.', dir=_BACKUP_STATE_DIR)
|
||||
try:
|
||||
try:
|
||||
with os.fdopen(tmp_fd, 'wb') as f:
|
||||
f.write(content)
|
||||
os.chmod(tmp_path, 0o600)
|
||||
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:
|
||||
return jsonify({
|
||||
'error': 'proxmox-backup-client did not recognise this file as a valid PBS keyfile',
|
||||
}), 400
|
||||
|
||||
# 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
|
||||
|
||||
try:
|
||||
os.replace(tmp_path, _PBS_KEYFILE_PATH)
|
||||
os.chmod(_PBS_KEYFILE_PATH, 0o600)
|
||||
except OSError as e:
|
||||
return jsonify({'error': f'cannot promote keyfile: {e}'}), 500
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'keyfile_path': _PBS_KEYFILE_PATH,
|
||||
'recovery_reset': True,
|
||||
})
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(tmp_path) and tmp_path != _PBS_KEYFILE_PATH:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/api/host-backups/pbs-recovery/setup', methods=['POST'])
|
||||
@require_auth
|
||||
def api_pbs_recovery_setup():
|
||||
@@ -14428,21 +14521,47 @@ def api_host_backups_job_create():
|
||||
import socket
|
||||
pbs_backup_id = f'hostcfg-{socket.gethostname()}'
|
||||
pbs_fingerprint = (payload.get('pbs_fingerprint') or '').strip()
|
||||
# Client-side encryption: when the operator toggled "encrypt"
|
||||
# we generate the host-wide keyfile (if missing) and pin it
|
||||
# in the .env. The runner sees PBS_KEYFILE and adds --keyfile
|
||||
# to the backup invocation.
|
||||
pbs_encrypt = bool(payload.get('pbs_encrypt'))
|
||||
# Client-side encryption. Three modes:
|
||||
# - "none" — no --keyfile flag, plain backup
|
||||
# - "new" — generate a fresh per-host keyfile (default,
|
||||
# matches legacy `pbs_encrypt: true`)
|
||||
# - "existing" — reuse whatever keyfile is already installed
|
||||
# at _PBS_KEYFILE_PATH (typical after the
|
||||
# operator uploaded their own via
|
||||
# /api/host-backups/pbs-encryption/import)
|
||||
# Legacy compat: a truthy `pbs_encrypt` bool with no
|
||||
# `pbs_encrypt_mode` maps to "new".
|
||||
pbs_encrypt_mode = (payload.get('pbs_encrypt_mode') or '').strip().lower()
|
||||
if not pbs_encrypt_mode:
|
||||
pbs_encrypt_mode = 'new' if bool(payload.get('pbs_encrypt')) else 'none'
|
||||
if pbs_encrypt_mode not in ('none', 'new', 'existing'):
|
||||
return jsonify({'error': "pbs_encrypt_mode must be 'none', 'new' or 'existing'"}), 400
|
||||
|
||||
lines.append(f'PBS_REPOSITORY={pbs_repo}')
|
||||
lines.append(f'PBS_PASSWORD={pbs_pass}')
|
||||
lines.append(f'PBS_BACKUP_ID={pbs_backup_id}')
|
||||
if pbs_fingerprint:
|
||||
lines.append(f'PBS_FINGERPRINT={pbs_fingerprint}')
|
||||
if pbs_encrypt:
|
||||
|
||||
if pbs_encrypt_mode == 'new':
|
||||
kf = _pbs_keyfile_get_or_create(True)
|
||||
if not kf:
|
||||
return jsonify({'error': 'failed to create PBS encryption keyfile (is proxmox-backup-client installed?)'}), 500
|
||||
lines.append(f'PBS_KEYFILE={kf}')
|
||||
elif pbs_encrypt_mode == 'existing':
|
||||
# The keyfile must already be present — either from a
|
||||
# previous run or from a fresh import via the dedicated
|
||||
# endpoint. Fail loudly rather than silently generating
|
||||
# a new one that would leave the user's own key unused.
|
||||
if not os.path.isfile(_PBS_KEYFILE_PATH):
|
||||
return jsonify({
|
||||
'error': (
|
||||
"pbs_encrypt_mode='existing' but no keyfile is installed at "
|
||||
f"{_PBS_KEYFILE_PATH} — import one first with "
|
||||
"POST /api/host-backups/pbs-encryption/import"
|
||||
),
|
||||
}), 400
|
||||
lines.append(f'PBS_KEYFILE={_PBS_KEYFILE_PATH}')
|
||||
elif backend == 'local':
|
||||
explicit = (payload.get('local_dest_dir') or '').strip()
|
||||
if explicit:
|
||||
@@ -14630,6 +14749,14 @@ def _backup_job_detail(env_file: str) -> dict:
|
||||
'pbs_fingerprint': raw.get('PBS_FINGERPRINT') or None,
|
||||
'has_pbs_password': bool(raw.get('PBS_PASSWORD')),
|
||||
'pbs_encrypt': bool(raw.get('PBS_KEYFILE')),
|
||||
# New: mode used the last time the job was created / edited.
|
||||
# The .env only stores PBS_KEYFILE=<path>, so we can't tell
|
||||
# "new" from "existing" after the fact — the edit flow just
|
||||
# sees "encrypted" and defaults to "new" if the operator
|
||||
# decides to reconfigure. Sending "new" whenever a keyfile is
|
||||
# present matches the legacy pbs_encrypt bool: an edit that
|
||||
# doesn't touch the mode dropdown produces the same .env.
|
||||
'pbs_encrypt_mode': ('new' if raw.get('PBS_KEYFILE') else 'none'),
|
||||
'local_dest_dir': raw.get('LOCAL_DEST_DIR') or None,
|
||||
'local_archive_ext': raw.get('LOCAL_ARCHIVE_EXT') or None,
|
||||
'borg_repo': raw.get('BORG_REPO') or None,
|
||||
|
||||
@@ -903,6 +903,56 @@ hb_pbs_decrypt_recovery() {
|
||||
-in "$1" -out "$2" -pass stdin 2>/dev/null
|
||||
}
|
||||
|
||||
# Import a user-provided PBS encryption keyfile at the canonical
|
||||
# ProxMenux path. Used by the "shared keyfile across hosts" flow
|
||||
# where the operator generated one master key manually and wants
|
||||
# every host to reuse it (single recovery blob covers all).
|
||||
#
|
||||
# Args: $1 = path to the source keyfile (any location on disk).
|
||||
#
|
||||
# Validation: the file must exist, be non-empty, and be recognised
|
||||
# by `proxmox-backup-client key info` as a valid PBS keyfile. If
|
||||
# any check fails the function returns non-zero and leaves the
|
||||
# canonical path untouched, so the caller can fall back to the
|
||||
# "generate new" path or report the error.
|
||||
#
|
||||
# On success: the file is copied to $HB_STATE_DIR/pbs-key.conf
|
||||
# with chmod 600 and the caller can proceed with the recovery
|
||||
# blob offer as usual.
|
||||
hb_pbs_import_keyfile() {
|
||||
local src="$1"
|
||||
local dst="$HB_STATE_DIR/pbs-key.conf"
|
||||
|
||||
[[ -n "$src" && -s "$src" && -r "$src" ]] || return 1
|
||||
|
||||
# `key info` reads the file header and prints fingerprint /
|
||||
# kdf info without prompting for a passphrase (this works for
|
||||
# kdf=none keyfiles, which is what our own `key create` uses
|
||||
# and what most operators generate manually). If the file is
|
||||
# not a valid PBS keyfile, the tool exits non-zero.
|
||||
if ! proxmox-backup-client key info --output-format json "$src" \
|
||||
>/dev/null 2>&1; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HB_STATE_DIR" 2>/dev/null || true
|
||||
# Install atomically: cp to a tmp path in the same dir, chmod,
|
||||
# then rename. Avoids a half-written file if the copy is
|
||||
# interrupted mid-flight.
|
||||
local tmp
|
||||
tmp=$(mktemp "${dst}.import.XXXXXX") || return 3
|
||||
if ! cp -f "$src" "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
return 3
|
||||
fi
|
||||
chmod 600 "$tmp"
|
||||
if ! mv -f "$tmp" "$dst"; then
|
||||
rm -f "$tmp"
|
||||
return 3
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
hb_pbs_setup_recovery() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
@@ -1110,6 +1160,97 @@ hb_pbs_try_keyfile_recovery() {
|
||||
}
|
||||
|
||||
|
||||
# Internal helper: create a fresh PBS keyfile at the canonical path,
|
||||
# offer the recovery passphrase, and export HB_PBS_KEYFILE_OPT.
|
||||
# Returns 0 on success, 1 on any failure or cancel (leaves state
|
||||
# clean — canonical path is wiped on cancel so the next attempt
|
||||
# starts fresh).
|
||||
_hb_pbs_create_new_keyfile() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
|
||||
msg_info "$(hb_translate "Creating PBS encryption key...")"
|
||||
mkdir -p "$HB_STATE_DIR"
|
||||
local create_stderr
|
||||
create_stderr=$(proxmox-backup-client key create --kdf none "$key_file" </dev/null 2>&1 >/dev/null)
|
||||
local create_rc=$?
|
||||
if [[ $create_rc -eq 0 && -s "$key_file" ]]; then
|
||||
chmod 600 "$key_file"
|
||||
msg_ok "$(hb_translate "Encryption key created:") $key_file"
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
if ! hb_pbs_setup_recovery; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create failed — sweep zero-byte residue and surface the real error.
|
||||
[[ -f "$key_file" && ! -s "$key_file" ]] && rm -f "$key_file" 2>/dev/null
|
||||
local err_msg
|
||||
err_msg="$(hb_translate "Failed to create encryption key. Backup cancelled — fix the underlying issue and retry.")"$'\n\n'
|
||||
err_msg+="$(hb_translate "Tool exit code:") $create_rc"$'\n'
|
||||
err_msg+="$(hb_translate "Tool output:")"$'\n'
|
||||
err_msg+="${create_stderr:-(empty)}"
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption key creation failed")" \
|
||||
--msgbox "$err_msg" 14 78
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Internal helper: prompt the operator for a source path, validate
|
||||
# with hb_pbs_import_keyfile, install at the canonical path, offer
|
||||
# the recovery passphrase, and export HB_PBS_KEYFILE_OPT. Returns
|
||||
# 0 on success, 1 on cancel or failure (state is left clean).
|
||||
_hb_pbs_import_dialog() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local src
|
||||
|
||||
src=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Import PBS keyfile")" \
|
||||
--inputbox "$(hb_translate "Absolute path to your existing PBS keyfile:")"$'\n\n'"$(hb_translate "The file is validated with 'proxmox-backup-client key info' and copied to")"$'\n'"$key_file $(hb_translate "with chmod 600.")" \
|
||||
14 78 "" 3>&1 1>&2 2>&3) || return 1
|
||||
src="$(echo "$src" | xargs)"
|
||||
[[ -z "$src" ]] && return 1
|
||||
|
||||
hb_pbs_import_keyfile "$src"
|
||||
local rc=$?
|
||||
case $rc in
|
||||
0)
|
||||
msg_ok "$(hb_translate "Imported existing encryption key:") $key_file"
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
# Recovery blob is still offered. When the same key lives
|
||||
# on multiple hosts, a single blob works for all — but
|
||||
# re-uploading from each host is idempotent and lets the
|
||||
# operator recover the key from PBS even if only ONE host
|
||||
# is left standing.
|
||||
if ! hb_pbs_setup_recovery; then
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
1)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "The file does not exist, is empty or is not readable.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
2)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "proxmox-backup-client did not recognise this file as a valid PBS keyfile.")"$'\n\n'"$(hb_translate "Path:") $src" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Import failed")" \
|
||||
--msgbox "$(hb_translate "Could not copy the keyfile into place. Check permissions on:") $HB_STATE_DIR" \
|
||||
10 78
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
hb_ask_pbs_encryption() {
|
||||
local key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
@@ -1120,98 +1261,67 @@ hb_ask_pbs_encryption() {
|
||||
# often the terminal title or a stray line from a prior manual
|
||||
# `proxmox-backup-client` invocation in the same SSH session.
|
||||
clear
|
||||
# Reset the window title in case a prior tool set it (the
|
||||
# `Encryption Key Password:` title that proxmox-backup-client
|
||||
# sets when prompting interactively, for instance — it sticks
|
||||
# around in xterm-compatible terminals until overwritten).
|
||||
printf '\033]0;ProxMenux\007'
|
||||
|
||||
# Wipe any zero-byte keyfile left behind by a previous cancelled or
|
||||
# failed key-create run. Without this the "existing keyfile" branch
|
||||
# below would happily hand `--keyfile <empty>` to
|
||||
# proxmox-backup-client, which then dies mid-backup with an
|
||||
# opaque "unable to load encryption key" error. Reported by a
|
||||
# user who cancelled at the passphrase step and then tried again.
|
||||
# Wipe any zero-byte keyfile left behind by a previous cancelled
|
||||
# or failed key-create run — otherwise the "existing" branch would
|
||||
# hand `--keyfile <empty>` to proxmox-backup-client and die mid
|
||||
# backup with an opaque "unable to load encryption key" error.
|
||||
if [[ -f "$key_file" && ! -s "$key_file" ]]; then
|
||||
rm -f "$key_file" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Build the menu. When a keyfile is already present the operator
|
||||
# can Use it as-is; otherwise the choice is Generate / Import /
|
||||
# Skip. Same shape both times so muscle memory works.
|
||||
local -a menu_items=()
|
||||
if [[ -s "$key_file" ]]; then
|
||||
# Already have a keyfile — one confirmation, no double yes/no.
|
||||
# An existing keyfile is trusted: it can only be here if a
|
||||
# previous encrypted backup completed (cancels in the setup
|
||||
# flow wipe the file — see the create path below).
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
|
||||
--yesno "$(hb_translate "Encrypt this backup with the existing keyfile?")"$'\n\n'"$(hb_translate "Location:") $key_file" \
|
||||
10 78 || return 0
|
||||
export HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
msg_ok "$(hb_translate "Using existing encryption key:") $key_file"
|
||||
return 0
|
||||
menu_items+=("existing" "$(hb_translate "Use existing keyfile at") $key_file")
|
||||
fi
|
||||
menu_items+=(
|
||||
"new" "$(hb_translate "Generate a new keyfile (one per host — safest isolation)")"
|
||||
"import" "$(hb_translate "Import an existing keyfile from a path (shared across hosts)")"
|
||||
"none" "$(hb_translate "Skip — no encryption for this backup")"
|
||||
)
|
||||
local menu_h=14
|
||||
(( ${#menu_items[@]} > 8 )) && menu_h=16
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
|
||||
--menu "$(hb_translate "Choose how to protect this backup:")" \
|
||||
"$menu_h" 78 4 "${menu_items[@]}" \
|
||||
3>&1 1>&2 2>&3) || return 0
|
||||
|
||||
# No key yet — single confirmation merges "encrypt?" + "generate keyfile?"
|
||||
# into one screen. The keyfile is generated with `--kdf none` (no
|
||||
# passphrase on the keyfile itself) because `proxmox-backup-client key
|
||||
# create` doesn't accept the passphrase via env/stdin; the operator
|
||||
# gets a recovery passphrase in the next step instead.
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
|
||||
--yesno "$(hb_translate "Encrypt this backup with a keyfile?")"$'\n\n'"$(hb_translate "A new keyfile will be generated automatically.")"$'\n'"$(hb_translate "Location:") $key_file"$'\n'"$(hb_translate "Protection: chmod 600 (no passphrase on the keyfile itself)")"$'\n\n'"$(hb_translate "Next step will offer a recovery passphrase so the keyfile can be retrieved from PBS if you lose this host.")" \
|
||||
16 80 || return 0
|
||||
|
||||
msg_info "$(hb_translate "Creating PBS encryption key...")"
|
||||
mkdir -p "$HB_STATE_DIR"
|
||||
local create_stderr
|
||||
create_stderr=$(proxmox-backup-client key create --kdf none "$key_file" </dev/null 2>&1 >/dev/null)
|
||||
local create_rc=$?
|
||||
# `-s` (nonzero size) rather than `-f` (exists) — a zero-byte file
|
||||
# is treated as if the create failed. Matches the defensive wipe
|
||||
# at the top of the function.
|
||||
if [[ $create_rc -eq 0 && -s "$key_file" ]]; then
|
||||
chmod 600 "$key_file"
|
||||
msg_ok "$(hb_translate "Encryption key created:") $key_file"
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
|
||||
# Recovery passphrase is REQUIRED when the operator chose
|
||||
# to encrypt. Encrypting without a recovery escrow lets the
|
||||
# keyfile live only on this host — one reinstall or disk
|
||||
# failure away from every encrypted backup being lost. If
|
||||
# the operator cancels the passphrase dialog: wipe the
|
||||
# freshly-created keyfile (avoid orphans in state-dir),
|
||||
# unset the --keyfile flag, and return 1 so the caller can
|
||||
# bail out and drop the operator back at the previous menu.
|
||||
if ! hb_pbs_setup_recovery; then
|
||||
# Cancel between dialogs: wipe residue silently and hand
|
||||
# control back. No terminal output — the outer menu will
|
||||
# re-render and printing anything here would break the
|
||||
# dialog-to-dialog transition.
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Key create failed. Sweep any zero-byte / half-written file
|
||||
# the tool may have left behind so the next attempt starts
|
||||
# from a clean state instead of picking up an unusable
|
||||
# "existing keyfile".
|
||||
[[ -f "$key_file" && ! -s "$key_file" ]] && rm -f "$key_file" 2>/dev/null
|
||||
# Surface the actual error from proxmox-backup-client and
|
||||
# abort the backup: the operator asked for encryption, so
|
||||
# silently downgrading to unencrypted (past behaviour) hid
|
||||
# the failure behind a modal that a hurried operator would
|
||||
# dismiss without reading. Returning 1 propagates cancel to
|
||||
# _bk_pbs, which drops back to the previous menu so the
|
||||
# underlying issue (perms, disk full, ...) can be fixed
|
||||
# before retrying.
|
||||
local err_msg
|
||||
err_msg="$(hb_translate "Failed to create encryption key. Backup cancelled — fix the underlying issue and retry.")"$'\n\n'
|
||||
err_msg+="$(hb_translate "Tool exit code:") $create_rc"$'\n'
|
||||
err_msg+="$(hb_translate "Tool output:")"$'\n'
|
||||
err_msg+="${create_stderr:-(empty)}"
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption key creation failed")" \
|
||||
--msgbox "$err_msg" 14 78
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
case "$choice" in
|
||||
existing)
|
||||
# An existing keyfile is trusted — it only lands here after
|
||||
# a previous encrypted backup completed successfully (the
|
||||
# create / import paths wipe the file on cancel).
|
||||
HB_PBS_KEYFILE_OPT="--keyfile $key_file"
|
||||
msg_ok "$(hb_translate "Using existing encryption key:") $key_file"
|
||||
return 0
|
||||
;;
|
||||
new)
|
||||
# Operator asked to Replace or first-time Generate — wipe
|
||||
# any prior keyfile + recovery blob so the create path
|
||||
# starts from a clean slate.
|
||||
if [[ -s "$key_file" ]]; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
fi
|
||||
_hb_pbs_create_new_keyfile
|
||||
return $?
|
||||
;;
|
||||
import)
|
||||
if [[ -s "$key_file" ]]; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
fi
|
||||
_hb_pbs_import_dialog
|
||||
return $?
|
||||
;;
|
||||
*)
|
||||
# "none" or unknown → no encryption for this backup.
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
|
||||
@@ -169,7 +169,29 @@ export default async function PbsDestinationPage({
|
||||
</h3>
|
||||
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("encryption.keyfileBody", { code })}
|
||||
{t.rich("encryption.keyfileBody", { code, em })}
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
|
||||
{t("encryption.modesTitle")}
|
||||
</h3>
|
||||
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("encryption.modesIntro", { em })}
|
||||
</p>
|
||||
|
||||
<h4 className="text-base font-semibold mt-4 mb-2 text-gray-900">
|
||||
{t("encryption.modesPerHostTitle")}
|
||||
</h4>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("encryption.modesPerHostBody", { code, em })}
|
||||
</p>
|
||||
|
||||
<h4 className="text-base font-semibold mt-4 mb-2 text-gray-900">
|
||||
{t("encryption.modesSharedTitle")}
|
||||
</h4>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("encryption.modesSharedBody", { code, em })}
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
|
||||
|
||||
@@ -52,7 +52,13 @@
|
||||
"heading": "Client-side encryption",
|
||||
"intro": "PBS client-side keyfile encryption encrypts chunks on the source host before upload. ProxMenux enables the feature with one added constraint: a recovery passphrase is mandatory when encryption is enabled. The passphrase does not protect the local keyfile; it protects the escrow copy of the keyfile that ProxMenux uploads to PBS for disaster recovery.",
|
||||
"keyfileTitle": "Keyfile",
|
||||
"keyfileBody": "On first use, <code>proxmox-backup-client key create --kdf none</code> generates the keyfile at <code>/usr/local/share/proxmenux/pbs-key.conf</code> (<code>chmod 600</code>). Subsequent backups reuse it after a single confirmation dialog. If key creation fails, the backup is cancelled and the tool's error output is shown in a dialog.",
|
||||
"keyfileBody": "Both the CLI wizard and the Monitor Web modal expose the encryption setup as a three-way choice: <em>generate a new keyfile</em>, <em>import an existing one</em>, or <em>skip</em>. <em>Generate</em> runs <code>proxmox-backup-client key create --kdf none</code> and writes the keyfile at <code>/usr/local/share/proxmenux/pbs-key.conf</code> (<code>chmod 600</code>). <em>Import</em> takes a keyfile the operator already has (typically the one used on other hosts) and installs it at the same path after validating it with <code>proxmox-backup-client key info</code>. Subsequent backups on the same host reuse whichever keyfile is installed after a single confirmation dialog. If any step fails, the backup is cancelled and the tool's error output is shown in a dialog.",
|
||||
"modesTitle": "Per-host or shared keyfile",
|
||||
"modesIntro": "Both operating models are supported and neither is enforced — the choice belongs to the operator based on how their fleet is organised.",
|
||||
"modesPerHostTitle": "Per-host keyfile (default)",
|
||||
"modesPerHostBody": "Each host generates its own keyfile the first time it enables PBS encryption. Isolation is maximum: compromising the keyfile of one host does not expose the backups of any other. Each host has its own paired recovery blob on PBS, restorable with the recovery passphrase of that host. Recommended for production fleets and for environments where hosts have different owners or compliance boundaries.",
|
||||
"modesSharedTitle": "Shared keyfile (import on every host)",
|
||||
"modesSharedBody": "One master keyfile generated once and installed on every host via the <em>Import</em> option. Management is simpler: a single secret to safeguard, a single recovery blob works for every host, and any host can decrypt the archives of any other (useful for consolidation, cross-host restore drills, or centralised backup verification). The trade-off is that a leak of the shared keyfile exposes every host at once. Recommended for homelabs and for fleets where all hosts have the same owner and trust boundary.",
|
||||
"recoveryTitle": "Recovery passphrase and escrow blob",
|
||||
"recoveryBody": "After the keyfile is created, ProxMenux prompts twice for a recovery passphrase (with match validation) and runs <code>openssl</code> to produce <code>pbs-key.recovery.enc</code> — the keyfile encrypted with the passphrase. A copy is written to <code>/root/pbs-key.recovery-HOSTNAME-YYYYMMDD.enc</code> for offsite storage. Cancelling the passphrase dialog wipes the freshly-created keyfile.",
|
||||
"blobUploadTitle": "Paired backup group on PBS",
|
||||
|
||||
@@ -52,7 +52,13 @@
|
||||
"heading": "Cifrado del lado del cliente",
|
||||
"intro": "El cifrado por keyfile del lado del cliente de PBS cifra los chunks en el host de origen antes de la subida. ProxMenux habilita esta funcionalidad con una restricción añadida: la passphrase de recuperación es obligatoria cuando se activa el cifrado. La passphrase no protege el keyfile local; protege la copia de escrow del keyfile que ProxMenux sube a PBS para recuperación ante desastre.",
|
||||
"keyfileTitle": "Keyfile",
|
||||
"keyfileBody": "En el primer uso, <code>proxmox-backup-client key create --kdf none</code> genera el keyfile en <code>/usr/local/share/proxmenux/pbs-key.conf</code> (<code>chmod 600</code>). Las copias posteriores lo reutilizan tras un único diálogo de confirmación. Si la creación del keyfile falla, la copia se cancela y la salida de error de la herramienta se muestra en un diálogo.",
|
||||
"keyfileBody": "Tanto el wizard de la CLI como el modal Web del Monitor exponen la configuración de cifrado como una elección de tres opciones: <em>generar un keyfile nuevo</em>, <em>importar uno existente</em> u <em>omitir</em>. <em>Generar</em> ejecuta <code>proxmox-backup-client key create --kdf none</code> y escribe el keyfile en <code>/usr/local/share/proxmenux/pbs-key.conf</code> (<code>chmod 600</code>). <em>Importar</em> toma un keyfile que el usuario ya tiene (habitualmente el mismo que usa en sus otros hosts) y lo instala en la misma ruta tras validarlo con <code>proxmox-backup-client key info</code>. Las copias posteriores en el mismo host reutilizan cualquier keyfile instalado tras un único diálogo de confirmación. Si algún paso falla, la copia se cancela y la salida de error de la herramienta se muestra en un diálogo.",
|
||||
"modesTitle": "Keyfile por host o compartido",
|
||||
"modesIntro": "Ambos modelos operativos están soportados y ninguno se impone — la decisión pertenece al usuario según cómo esté organizada su flota.",
|
||||
"modesPerHostTitle": "Keyfile por host (por defecto)",
|
||||
"modesPerHostBody": "Cada host genera su propio keyfile la primera vez que activa el cifrado PBS. El aislamiento es máximo: comprometer el keyfile de un host no expone las copias de ningún otro. Cada host tiene su propio blob de recuperación emparejado en PBS, restaurable con la passphrase de recuperación de ese host. Recomendado para flotas de producción y para entornos donde los hosts tienen propietarios o límites de compliance distintos.",
|
||||
"modesSharedTitle": "Keyfile compartido (importar en cada host)",
|
||||
"modesSharedBody": "Un keyfile maestro generado una vez e instalado en cada host mediante la opción <em>Importar</em>. La gestión es más simple: un único secreto que proteger, un único blob de recuperación sirve para todos los hosts, y cualquier host puede desencriptar los archives de cualquier otro (útil para consolidación, simulacros de restore cruzado o verificación centralizada de backups). El trade-off es que una filtración del keyfile compartido expone todos los hosts a la vez. Recomendado para homelabs y para flotas donde todos los hosts tienen el mismo propietario y límite de confianza.",
|
||||
"recoveryTitle": "Passphrase de recuperación y blob de escrow",
|
||||
"recoveryBody": "Tras crear el keyfile, ProxMenux pregunta dos veces por una passphrase de recuperación (con validación de coincidencia) y ejecuta <code>openssl</code> para producir <code>pbs-key.recovery.enc</code> — el keyfile cifrado con la passphrase. Se escribe una copia en <code>/root/pbs-key.recovery-HOSTNAME-YYYYMMDD.enc</code> para almacenamiento offsite. Cancelar el diálogo de la passphrase borra el keyfile recién creado.",
|
||||
"blobUploadTitle": "Grupo emparejado en PBS",
|
||||
|
||||
Reference in New Issue
Block a user