* fix: verify checksums for downloaded ISOs/images, keep sshpass off argv ensure-base-iso.sh downloaded the PVE install ISO over plain HTTP with no checksum, caching it on the persistent /opt/pve-integration mount and booting it as the nested trust root the integration suite relies on. ensure-cloud-images.sh fetched the Ubuntu cloud image and OVA over HTTPS but never checked them either. prepare-test-environment.sh and diagnose-cluster.sh passed the nested root password to sshpass via -p, putting it in the process table. create-api-token.sh, unused anywhere in the repo, minted a privsep=0 root token and echoed the secret unmasked. - ensure-base-iso.sh now downloads from https://enterprise.proxmox.com/iso and verifies against its SHA256SUMS on every run, including a cache hit. download.proxmox.com's own TLS cert does not list download.proxmox.com in its SAN (confirmed with curl/openssl from this environment), so https to that name fails certificate validation; enterprise.proxmox.com serves the identical ISO tree over a valid cert. Verification happens before the downloaded file is moved to its canonical cache path. - ensure-cloud-images.sh verifies the cloud image and OVA against Ubuntu's published SHA256SUMS the same way, matching by upstream filename since the cloud image is cached locally under a different extension (.img upstream, .qcow2 cached — the bytes are already qcow2-formatted). - prepare-test-environment.sh and diagnose-cluster.sh now export SSHPASS and call sshpass -e, keeping the password out of argv/ps. This also fixes a latent bug: the old unquoted `sshpass -p ${ROOT_PASS}` word-split any password containing whitespace. - create-api-token.sh deleted; grep across the repo found no caller. Reviewers (codex:codex-rescue, correctness-reviewer, security-reviewer) all independently found the same blocking bug in the first pass: when a cached file failed verification and the subsequent redownload then failed, ensure-cloud-images.sh fell through to a "keep the stale copy" branch and returned that same known-bad file with exit 0 — verification could be bypassed by inducing one failed redownload. Fixed by deleting the file immediately on a failed verification, before the redownload is attempted, so the later "is there a safe stale copy" check can no longer find it. Added a test case (case 5) that reproduces this exact sequence and mutation-tested it against the unfixed code. The three reviews also flagged a real but separate bug already fixed in this same change: `trap ... RETURN` inside a function nested in another function is not scoped to that function in bash — it re-fires on the OUTER function's return, referencing an out-of-scope local. Both verify_checksum() helpers now clean up their temp file explicitly instead of via trap. Findings not acted on, judged out of scope for this fix: - SHA256SUMS-fetch failures are treated the same as a checksum mismatch (delete + fail) rather than left untouched — a transient network blip destroys a good multi-GB cached ISO. This is the safer failure direction (never silently trust unverified bytes) and was a deliberate trade-off, not a defect. - ensure-cloud-images.sh's 7-day cache window can span an upstream republish of noble/current, causing a legitimate re-verification churn (not a security issue, a cache-hit-rate one). Pre-existing cache design, unrelated to adding verification. - wait-for-pve.sh (curl -d with the password on argv) and prepare-test-environment.sh's own positional password argument (from run-integration.sh) carry the same password-on-argv pattern this issue targeted in create-api-token.sh, sshpass -p and diagnose-cluster.sh, but neither script nor run-integration.sh was named in the issue. Left untouched per scope; worth a follow-up issue. - GPG/detached-signature verification of the upstream SHA256SUMS was not added — the new checks defend against cache poisoning and transit corruption, not a compromised origin. Worth a follow-up issue. - The two new self-checks (ensure-base-iso.test.sh, ensure-cloud-images.test.sh) are not wired into .github/workflows/unit-tests.yml's shell-selfchecks job. That file is code-owned and out of scope for this change; needs an operator follow-up. Password rotation (the Testpass123! value from before it moved to a secret) is unaddressed here per the contract — flagged for the operator. Mutation-tested: broke the post-download checksum check in ensure-base-iso.sh, confirmed the affected test cases failed, restored it. Broke the sshpass -e change back to -p, confirmed the new assertions in prepare-test-environment.test.sh failed, restored it. Broke the fail-open fix in ensure-cloud-images.sh, confirmed case 5 failed, restored it. Closes #149 * fix: also verify the stale-by-age fallback copy in ensure-cloud-images.sh PR review on #166 (COMMENTED, non-blocking) found the sibling of the fail-open bug already fixed in this branch: when the cached cloud image is stale by *age* (>= 7 days) rather than failed verification, the redownload-failure fallback could hand back that file with exit 0 without ever re-verifying it in this run. A file that failed the earlier verification is already deleted by the time the fallback runs, but a stale-by-age file skips verification entirely on the way in. Fixed by verifying the stale-by-age file at the point of actual fallback use — after the redownload has failed, not proactively before it's attempted, so a copy the redownload was about to replace anyway isn't deleted along a path that would have succeeded. Added two test cases (6, 7): a still-verifying stale-by-age copy is used as a fallback; one that no longer verifies is not. Mutation-tested by reverting to the unfixed fallback and confirming case 7 fails, then restored. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
Nested Proxmox VE Test Infrastructure
This Terraform configuration provisions a throwaway nested Proxmox VE virtual machine on an existing Proxmox host. The nested instance is used as a target for PSProxmoxVE integration tests, providing a real PVE API to test against without risking production infrastructure.
Prerequisites
- Terraform >= 1.5.0
- Proxmox VE ISO downloaded from proxmox.com/en/downloads
- API token on the existing Proxmox host with full administrator privileges (Datastore.Allocate, VM.Allocate, VM.Config.*, Sys.Modify, etc.)
- curl and jq installed on the machine running Terraform (used by provisioner scripts)
- SSH agent running with a key that can access the Proxmox host (used by the bpg/proxmox provider for file uploads)
- A routable IP address available for the nested PVE instance (see Network section below)
- Nested virtualization enabled on the Proxmox host (see Intel vs AMD notes below)
Quick Start
-
Copy the example variables file and fill in your values:
cp terraform.tfvars.example terraform.tfvars # Edit terraform.tfvars with your Proxmox host details, ISO path, and network config -
Initialize Terraform and download the provider:
terraform init -
Review the plan:
terraform plan -
Apply to create the nested PVE VM:
terraform applyThis will:
- Upload the PVE ISO to the target node
- Generate an answer file for unattended installation
- Create and start the nested VM
- Wait for the PVE API to become responsive (up to 10 minutes)
- Create an API token (
root@pam!integration) for integration tests
-
After apply completes, retrieve the test connection details:
terraform output pve_test_url terraform output -raw pve_test_api_token
Preparing the Installer ISO (one-time per PVE version)
The standard PVE installer ISO has no automated install mode. Before first use — and again whenever you upgrade to a new PVE release — run the following on the Proxmox host to produce a modified ISO that tells the installer to fetch its answer file from an attached FAT partition labeled PROXMOX-AIS:
ssh root@<proxmox-host>
# Paths are examples — adjust to match your NAS mount points
ORIGINAL_ISO=/mnt/pve/nas-nfs/template/iso/proxmox-ve_9.1-1.iso
MODIFIED_ISO=/mnt/pve/nas-nfs/template/iso/proxmox-ve_9.1-1-auto.iso
proxmox-auto-install-assistant prepare-iso "$ORIGINAL_ISO" \
--fetch-from partition \
--partition-label proxmox-ais \
--output "$MODIFIED_ISO"
Update iso_file_id in terraform.tfvars to point to the new *-auto.iso file. The original ISO is left untouched.
How It Works
Answer File
The answer.toml.tftpl template generates a TOML answer file that automates the Proxmox VE installer. It configures the root password, network settings (static IP), disk layout, and other installation parameters so that no manual interaction is required during installation.
Wait Script
After the VM is created and booted from the ISO, the scripts/wait-for-api.sh script polls the nested PVE API endpoint every 10 seconds for up to 10 minutes. Installation typically takes 3-7 minutes depending on disk and CPU performance. The script exits successfully once the API returns a valid version response.
API Token Creation
Once the API is responsive, a provisioner script authenticates to the nested PVE using the root password and creates an API token (root@pam!integration) with full privileges (privsep=0). The token value is saved to .api-token and exposed via the pve_test_api_token output for use in integration tests.
Cleanup
To destroy the nested PVE VM and all associated resources:
terraform destroy
This removes the VM, uploaded ISO, and answer file snippet from the Proxmox host. The .api-token file is also cleaned up locally.
Network Configuration
The nested PVE instance requires a static IP address that is:
- Routable from the machine running integration tests (CI runner or developer workstation)
- Not in use by any other device on the network
- On the same subnet as the network bridge (
vmbr0by default) on the host
The nested PVE will configure its own vmbr0 bridge internally, but from the host's perspective it appears as a single VM with the assigned IP address.
If your test environment uses VLANs or an isolated test network, adjust the network_bridge variable accordingly and ensure the CI runner has connectivity to that network.
Resource Requirements
The nested PVE VM requires sufficient resources to run Proxmox VE and potentially host lightweight test VMs inside it:
| Resource | Minimum | Default | Recommended |
|---|---|---|---|
| CPU cores | 2 | 4 | 4+ |
| Memory | 4096 MB | 8192 MB | 8192+ MB |
| Disk | 32 GB | 64 GB | 64+ GB |
Ensure the Proxmox host has enough free resources to accommodate these allocations.
Intel vs AMD Nested Virtualization
Nested virtualization must be enabled on the host for the nested PVE to function as a hypervisor itself. The CPU type is set to host to pass through virtualization extensions.
Intel
Nested virtualization is typically enabled by default on modern Intel CPUs. Verify with:
cat /sys/module/kvm_intel/parameters/nested
If it shows N, enable it:
echo "options kvm_intel nested=1" > /etc/modprobe.d/kvm-intel.conf
modprobe -r kvm_intel && modprobe kvm_intel
AMD
AMD nested virtualization support varies. Check with:
cat /sys/module/kvm_amd/parameters/nested
If it shows 0, enable it:
echo "options kvm_amd nested=1" > /etc/modprobe.d/kvm-amd.conf
modprobe -r kvm_amd && modprobe kvm_amd
Note that AMD nested virtualization can be less stable than Intel in some configurations. If you encounter issues with nested VMs inside the nested PVE, the integration tests themselves (which test the PVE API, not nested VM creation) will still work -- only tests that attempt to create VMs inside the nested PVE would be affected.
Files
| File | Purpose |
|---|---|
main.tf |
Provider config, VM resource, provisioners |
variables.tf |
Input variable definitions with defaults |
outputs.tf |
Test connection details for integration tests |
storage-vm.tf |
Shared NFS/iSCSI/answer-server VM on the CI VLAN |
answer.toml.tftpl |
Unattended PVE installer answer file template |
scripts/setup-storage-server.sh |
Configures services on the storage VM over SSH |
scripts/wait-for-api.sh |
Polls PVE API until responsive |
terraform.tfvars.example |
Example variable values |
.gitignore |
Excludes state, provider cache, secrets |