F-LEAK third attempt: band-scoped fenced destroy (v0.110.0)
The per-VM ACL is consumed by the destroy it authorises (PVE remove_vm_access, LXC.pm:906), so it works once per slot. Fourth root-fenced exception, band-enforced in sudoers literally + in code + at the caller. API destroy still tried first.
This commit is contained in:
@@ -136,6 +136,39 @@ func (p *Privileged) CreateGoldenLXC(ctx context.Context, spec GoldenLXCSpec) er
|
||||
return p.run(ctx, "pct", args...)
|
||||
}
|
||||
|
||||
// DestroyScratchLXC destroys a restore-test scratch guest through the fenced root path, refusing any
|
||||
// vmid outside the caller-supplied scratch band.
|
||||
//
|
||||
// WHY THIS CANNOT BE THE API — and this is the fourth fenced exception, so the reasoning is recorded
|
||||
// in full. A restore-test whose restore FAILS leaves a scratch guest the API token cannot destroy:
|
||||
// `FelhomAgentGuest` is granted at /pool/felhom, and a guest joins that pool only when its restore
|
||||
// COMPLETES. A failed restore therefore leaves a guest that exists, is in no pool, and is out of
|
||||
// reach (403 VM.Allocate) while holding its disks.
|
||||
//
|
||||
// TWO API-SIDE FIXES WERE BUILT AND BOTH REFUTED BY LIVE TEST on 2026-07-28:
|
||||
// - Adopt the stranded guest into the pool, then retry. `PUT /pools/{pool}` ALSO requires
|
||||
// VM.Allocate on the VM being added, so pool membership cannot bootstrap its own authority.
|
||||
// - Grant the role per-path at /vms/990000..990009. Durable for exactly ONE use per slot: PVE's own
|
||||
// destroy calls AccessControl::remove_vm_access (LXC.pm:906), deleting every ACL at /vms/<vmid>
|
||||
// (AccessControl.pm:1898). The grant is consumed by the operation it authorises.
|
||||
//
|
||||
// The band ACLs are still provisioned (host-install v1.21.0) and the API path is still tried FIRST —
|
||||
// this is the fallback that makes teardown deterministic rather than once-per-slot.
|
||||
//
|
||||
// THE FENCE. The band is enforced in THREE places, deliberately: sudoers matches the vmid literally
|
||||
// (`pct destroy 99000[0-9] --purge` — even a compromised agent asking for 9201 is refused by sudo
|
||||
// itself), this method re-checks it before exec, and the caller checks its own journal provenance.
|
||||
// Unlike an ACL, none of these is consumed by use.
|
||||
func (p *Privileged) DestroyScratchLXC(ctx context.Context, vmid, bandMin, bandMax int) error {
|
||||
if bandMin <= 0 || bandMax < bandMin {
|
||||
return fmt.Errorf("proxmox: DestroyScratchLXC needs a configured scratch band, got [%d,%d]", bandMin, bandMax)
|
||||
}
|
||||
if vmid < bandMin || vmid > bandMax {
|
||||
return fmt.Errorf("proxmox: refusing to destroy vmid %d — outside the scratch band [%d,%d]", vmid, bandMin, bandMax)
|
||||
}
|
||||
return p.run(ctx, "pct", "destroy", strconv.Itoa(vmid), "--purge")
|
||||
}
|
||||
|
||||
// MountUSBByUUID mounts a filesystem by UUID at target (creating the mountpoint).
|
||||
//
|
||||
// WHY THIS CANNOT BE THE API: a physical host mount is not a Proxmox API op; it is
|
||||
|
||||
Reference in New Issue
Block a user